Merge branch 'develop' into payment-reco-company-field

This commit is contained in:
Gursheen Kaur Anand 2024-01-25 11:44:53 +05:30 committed by GitHub
commit 3f383d81bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 1757 additions and 538 deletions

View File

@ -36,16 +36,16 @@
} }
}, },
"Fixed Assets": { "Fixed Assets": {
"Capital Equipments": { "Capital Equipment": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Electronic Equipments": { "Electronic Equipment": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Furnitures and Fixtures": { "Furniture and Fixtures": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Office Equipments": { "Office Equipment": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Plants and Machineries": { "Plants and Machineries": {

View File

@ -23,13 +23,13 @@ def get():
_("Tax Assets"): {"is_group": 1}, _("Tax Assets"): {"is_group": 1},
}, },
_("Fixed Assets"): { _("Fixed Assets"): {
_("Capital Equipments"): {"account_type": "Fixed Asset"}, _("Capital Equipment"): {"account_type": "Fixed Asset"},
_("Electronic Equipments"): {"account_type": "Fixed Asset"}, _("Electronic Equipment"): {"account_type": "Fixed Asset"},
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset"}, _("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
_("Office Equipments"): {"account_type": "Fixed Asset"}, _("Office Equipment"): {"account_type": "Fixed Asset"},
_("Plants and Machineries"): {"account_type": "Fixed Asset"}, _("Plants and Machineries"): {"account_type": "Fixed Asset"},
_("Buildings"): {"account_type": "Fixed Asset"}, _("Buildings"): {"account_type": "Fixed Asset"},
_("Softwares"): {"account_type": "Fixed Asset"}, _("Software"): {"account_type": "Fixed Asset"},
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"}, _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
_("CWIP Account"): { _("CWIP Account"): {
"account_type": "Capital Work in Progress", "account_type": "Capital Work in Progress",

View File

@ -36,13 +36,13 @@ def get():
"account_number": "1100-1600", "account_number": "1100-1600",
}, },
_("Fixed Assets"): { _("Fixed Assets"): {
_("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"}, _("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
_("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"}, _("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"}, _("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
_("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"}, _("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"}, _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"}, _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
_("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"}, _("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
_("Accumulated Depreciation"): { _("Accumulated Depreciation"): {
"account_type": "Accumulated Depreciation", "account_type": "Accumulated Depreciation",
"account_number": "1780", "account_number": "1780",

View File

@ -119,7 +119,7 @@ class TestAccount(unittest.TestCase):
InvalidAccountMergeError, InvalidAccountMergeError,
merge_account, merge_account,
"Capital Stock - _TC", "Capital Stock - _TC",
"Softwares - _TC", "Software - _TC",
) )
# Raise error as currency doesn't match # Raise error as currency doesn't match

View File

@ -55,7 +55,7 @@ class BankAccount(Document):
def validate_company(self): def validate_company(self):
if self.is_company_account and not self.company: if self.is_company_account and not self.company:
frappe.throw(_("Company is manadatory for company account")) frappe.throw(_("Company is mandatory for company account"))
def validate_iban(self): def validate_iban(self):
""" """

View File

@ -48,11 +48,11 @@ class BankGuarantee(Document):
def on_submit(self): def on_submit(self):
if not self.bank_guarantee_number: if not self.bank_guarantee_number:
frappe.throw(_("Enter the Bank Guarantee Number before submittting.")) frappe.throw(_("Enter the Bank Guarantee Number before submitting."))
if not self.name_of_beneficiary: if not self.name_of_beneficiary:
frappe.throw(_("Enter the name of the Beneficiary before submittting.")) frappe.throw(_("Enter the name of the Beneficiary before submitting."))
if not self.bank: if not self.bank:
frappe.throw(_("Enter the name of the bank or lending institution before submittting.")) frappe.throw(_("Enter the name of the bank or lending institution before submitting."))
@frappe.whitelist() @frappe.whitelist()

View File

@ -80,7 +80,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"depends_on": "eval: doc.coupon_type == \"Promotional\"", "depends_on": "eval: doc.coupon_type == \"Promotional\"",
@ -115,7 +115,7 @@
"read_only": 1 "read_only": 1
} }
], ],
"modified": "2019-10-19 14:48:14.602481", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Coupon Code", "name": "Coupon Code",

View File

@ -82,7 +82,7 @@
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2020-11-05 12:16:53.081573", "modified": "2024-01-17 13:06:01.608953",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Fiscal Year", "name": "Fiscal Year",
@ -118,6 +118,14 @@
{ {
"read": 1, "read": 1,
"role": "Employee" "role": "Employee"
},
{
"read": 1,
"role": "Accounts Manager"
},
{
"read": 1,
"role": "Stock Manager"
} }
], ],
"show_name_in_global_search": 1, "show_name_in_global_search": 1,

View File

@ -154,7 +154,7 @@ frappe.ui.form.on('Invoice Discounting', {
} }
}); });
}, },
primary_action_label: __('Get Invocies') primary_action_label: __('Get Invoices')
}); });
d.show(); d.show();
}, },

View File

@ -186,9 +186,12 @@ class JournalEntry(AccountsController):
def update_advance_paid(self): def update_advance_paid(self):
advance_paid = frappe._dict() advance_paid = frappe._dict()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
for d in self.get("accounts"): for d in self.get("accounts"):
if d.is_advance: if d.is_advance:
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"): if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name) advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items(): for voucher_type, order_list in advance_paid.items():

View File

@ -270,7 +270,7 @@ def start_import(invoices):
errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>" errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>"
), ),
indicator="red", indicator="red",
title=_("Error Occured"), title=_("Error Occurred"),
) )
return names return names

View File

@ -640,7 +640,7 @@ frappe.ui.form.on('Payment Entry', {
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
const today = frappe.datetime.get_today(); const today = frappe.datetime.get_today();
const fields = [ let fields = [
{fieldtype:"Section Break", label: __("Posting Date")}, {fieldtype:"Section Break", label: __("Posting Date")},
{fieldtype:"Date", label: __("From Date"), {fieldtype:"Date", label: __("From Date"),
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)}, fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
@ -655,18 +655,29 @@ frappe.ui.form.on('Payment Entry', {
fieldname:"outstanding_amt_greater_than", default: 0}, fieldname:"outstanding_amt_greater_than", default: 0},
{fieldtype:"Column Break"}, {fieldtype:"Column Break"},
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"}, ];
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() { if (frm.dimension_filters) {
return { let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
"filters": {"company": frm.doc.company}
fields.push({fieldtype:"Section Break"});
frm.dimension_filters.map((elem, idx)=>{
fields.push({
fieldtype: "Link",
label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label,
options: elem.document_type,
fieldname: elem.fieldname || elem.document_type
});
if(idx+1 == column_break_insertion_point) {
fields.push({fieldtype:"Column Break"});
} }
});
} }
},
{fieldtype:"Column Break"}, fields = fields.concat([
{fieldtype:"Section Break"}, {fieldtype:"Section Break"},
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
]; ]);
let btn_text = ""; let btn_text = "";

View File

@ -13,6 +13,7 @@ from pypika import Case
from pypika.functions import Coalesce, Sum from pypika.functions import Coalesce, Sum
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.bank_account.bank_account import ( from erpnext.accounts.doctype.bank_account.bank_account import (
get_bank_account_details, get_bank_account_details,
get_party_bank_account, get_party_bank_account,
@ -189,7 +190,7 @@ class PaymentEntry(AccountsController):
def set_liability_account(self): def set_liability_account(self):
# Auto setting liability account should only be done during 'draft' status # Auto setting liability account should only be done during 'draft' status
if self.docstatus > 0: if self.docstatus > 0 or self.payment_type == "Internal Transfer":
return return
if not frappe.db.get_value( if not frappe.db.get_value(
@ -925,7 +926,10 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float: def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0 base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if d.reference_doctype in advance_payment_doctypes:
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type. # When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes # This is so there are no Exchange Gain/Loss generated for such doctypes
@ -1423,8 +1427,11 @@ class PaymentEntry(AccountsController):
def update_advance_paid(self): def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party: if self.payment_type in ("Receive", "Pay") and self.party:
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc( frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid() ).set_total_advance_paid()
@ -1671,6 +1678,13 @@ def get_outstanding_reference_documents(args, validate=False):
condition += " and cost_center='%s'" % args.get("cost_center") condition += " and cost_center='%s'" % args.get("cost_center")
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = { date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"], "posting_date": ["from_posting_date", "to_posting_date"],
"due_date": ["from_due_date", "to_due_date"], "due_date": ["from_due_date", "to_due_date"],
@ -1904,6 +1918,12 @@ def get_orders_to_be_billed(
if doc and hasattr(doc, "cost_center") and doc.cost_center: if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center condition = " and cost_center='%s'" % cost_center
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
if party_account_currency == company_currency: if party_account_currency == company_currency:
grand_total_field = "base_grand_total" grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total" rounded_total_field = "base_rounded_total"

View File

@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type(__('Allocate'), null, 'default'); this.frm.change_custom_button_type(__('Allocate'), null, 'default');
} }
this.frm.trigger("set_query_for_dimension_filters");
// check for any running reconciliation jobs // check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) { if (this.frm.doc.receivable_payable_account) {
this.frm.call({ this.frm.call({
@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
} }
} }
set_query_for_dimension_filters() {
frappe.call({
method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters",
args: {
company: this.frm.doc.company,
},
callback: (r) => {
if (!r.exc && r.message) {
r.message.forEach(x => {
this.frm.set_query(x.fieldname, () => {
return {
'filters': x.filters
};
});
});
}
}
});
}
company() { company() {
this.frm.set_value('party', ''); this.frm.set_value('party', '');

View File

@ -25,7 +25,9 @@
"invoice_limit", "invoice_limit",
"payment_limit", "payment_limit",
"bank_cash_account", "bank_cash_account",
"accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break",
"sec_break1", "sec_break1",
"invoice_name", "invoice_name",
"invoices", "invoices",
@ -209,6 +211,18 @@
"fieldname": "payment_name", "fieldname": "payment_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Filter on Payment" "label": "Filter on Payment"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.invoices.length == 0",
"depends_on": "eval:doc.receivable_payable_account",
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions Filter"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,

View File

@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_any_doc_running, is_any_doc_running,
) )
@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions = [] self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = [] self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = [] self.ple_posting_date_filter = []
self.dimensions = get_dimensions()[0]
def load_from_db(self): def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly. # 'modified' attribute is required for `run_doc_method` to work properly.
@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
if self.payment_name: if self.payment_name:
condition.update({"name": self.payment_name}) condition.update({"name": self.payment_name})
# pass dynamic dimension filter values to query builder
dimensions = {}
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
dimensions.update({dimension: self.get(dimension)})
condition.update({"accounting_dimensions": dimensions})
payment_entries = get_advance_payment_entries_for_regional( payment_entries = get_advance_payment_entries_for_regional(
self.party_type, self.party_type,
self.party, self.party,
@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
return payment_entries return payment_entries
def get_jv_entries(self): def get_jv_entries(self):
condition = self.get_conditions() je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
conditions = self.get_journal_filter_conditions()
# Dimension filters
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
conditions.append(jea[dimension] == self.get(dimension))
if self.payment_name: if self.payment_name:
condition += f" and t1.name like '%%{self.payment_name}%%'" conditions.append(je.name.like(f"%%{self.payment_name}%%"))
if self.get("cost_center"): if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' " conditions.append(jea.cost_center == self.cost_center)
dr_or_cr = ( dr_or_cr = (
"credit_in_account_currency" "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable" if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency" else "debit_in_account_currency"
) )
conditions.append(jea[dr_or_cr].gt(0))
bank_account_condition = ( if self.bank_cash_account:
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
journal_query = (
qb.from_(je)
.inner_join(jea)
.on(jea.parent == je.name)
.select(
ConstantColumn("Journal Entry").as_("reference_type"),
je.name.as_("reference_name"),
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
jea[dr_or_cr].as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),
jea.cost_center.as_("cost_center"),
)
.where(
(je.docstatus == 1)
& (jea.party_type == self.party_type)
& (jea.party == self.party)
& (jea.account == self.receivable_payable_account)
& (
(jea.reference_type == "")
| (jea.reference_type.isnull())
| (jea.reference_type.isin(("Sales Order", "Purchase Order")))
)
)
.where(Criterion.all(conditions))
.orderby(je.posting_date)
) )
limit = f"limit {self.payment_limit}" if self.payment_limit else " " if self.payment_limit:
journal_query = journal_query.limit(self.payment_limit)
# nosemgrep journal_entries = journal_query.run(as_dict=True)
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency, t2.cost_center as cost_center
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
and t2.party_type = %(party_type)s and t2.party = %(party)s
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
and (t2.reference_type is null or t2.reference_type = '' or
(t2.reference_type in ('Sales Order', 'Purchase Order')
and t2.reference_name is not null and t2.reference_name != ''))
and (CASE
WHEN t1.voucher_type in ('Debit Note', 'Credit Note')
THEN 1=1
ELSE {bank_account_condition}
END)
order by t1.posting_date
{limit}
""".format(
**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"condition": condition,
"limit": limit,
}
),
{
"party_type": self.party_type,
"party": self.party,
"account": self.receivable_payable_account,
"bank_cash_account": "%%%s%%" % self.bank_cash_account,
},
as_dict=1,
)
return list(journal_entries) return list(journal_entries)
@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None, max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
get_payments=True, get_payments=True,
accounting_dimensions=self.accounting_dimension_filter_conditions,
) )
for inv in return_outstanding: for inv in return_outstanding:
@ -447,8 +459,15 @@ class PaymentReconciliation(Document):
row = self.append("allocation", {}) row = self.append("allocation", {})
row.update(entry) row.update(entry)
def update_dimension_values_in_allocated_entries(self, res):
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
res[dimension] = self.get(dimension)
return res
def get_allocated_entry(self, pay, inv, allocated_amount): def get_allocated_entry(self, pay, inv, allocated_amount):
return frappe._dict( res = frappe._dict(
{ {
"reference_type": pay.get("reference_type"), "reference_type": pay.get("reference_type"),
"reference_name": pay.get("reference_name"), "reference_name": pay.get("reference_name"),
@ -464,6 +483,9 @@ class PaymentReconciliation(Document):
} }
) )
res = self.update_dimension_values_in_allocated_entries(res)
return res
def reconcile_allocations(self, skip_ref_details_update_for_pe=False): def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
adjust_allocations_for_taxes(self) adjust_allocations_for_taxes(self)
dr_or_cr = ( dr_or_cr = (
@ -486,10 +508,10 @@ class PaymentReconciliation(Document):
reconciled_entry.append(payment_details) reconciled_entry.append(payment_details)
if entry_list: if entry_list:
reconcile_against_document(entry_list, skip_ref_details_update_for_pe) reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions)
if dr_or_cr_notes: if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company) reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
@frappe.whitelist() @frappe.whitelist()
def reconcile(self): def reconcile(self):
@ -518,7 +540,7 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries() self.get_unreconciled_entries()
def get_payment_details(self, row, dr_or_cr): def get_payment_details(self, row, dr_or_cr):
return frappe._dict( payment_details = frappe._dict(
{ {
"voucher_type": row.get("reference_type"), "voucher_type": row.get("reference_type"),
"voucher_no": row.get("reference_name"), "voucher_no": row.get("reference_name"),
@ -541,6 +563,12 @@ class PaymentReconciliation(Document):
} }
) )
for x in self.dimensions:
if row.get(x.fieldname):
payment_details[x.fieldname] = row.get(x.fieldname)
return payment_details
def check_mandatory_to_fetch(self): def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname): if not self.get(fieldname):
@ -648,6 +676,13 @@ class PaymentReconciliation(Document):
if not invoices_to_reconcile: if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table")) frappe.throw(_("No records found in Allocation table"))
def build_dimensions_filter_conditions(self):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear() self.common_filter_conditions.clear()
self.accounting_dimension_filter_conditions.clear() self.accounting_dimension_filter_conditions.clear()
@ -671,40 +706,30 @@ class PaymentReconciliation(Document):
if self.to_payment_date: if self.to_payment_date:
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date)) self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
def get_conditions(self, get_payments=False): self.build_dimensions_filter_conditions()
condition = " and company = '{0}' ".format(self.company)
if self.get("cost_center") and get_payments: def get_journal_filter_conditions(self):
condition = " and cost_center = '{0}' ".format(self.cost_center) conditions = []
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
conditions.append(je.company == self.company)
condition += ( if self.from_payment_date:
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) conditions.append(je.posting_date.gte(self.from_payment_date))
if self.from_payment_date
else "" if self.to_payment_date:
) conditions.append(je.posting_date.lte(self.to_payment_date))
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.minimum_payment_amount: if self.minimum_payment_amount:
condition += ( conditions.append(je.total_debit.gte(self.minimum_payment_amount))
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount: if self.maximum_payment_amount:
condition += ( conditions.append(je.total_debit.lte(self.maximum_payment_amount))
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
return condition return conditions
def reconcile_dr_cr_note(dr_cr_notes, company): def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes: for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
@ -754,6 +779,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
} }
) )
# Credit Note(JE) will inherit the same dimension values as payment
dimensions_dict = frappe._dict()
if active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = inv.get(dim.fieldname)
jv.accounts[0].update(dimensions_dict)
jv.accounts[1].update(dimensions_dict)
jv.flags.ignore_mandatory = True jv.flags.ignore_mandatory = True
jv.flags.ignore_exchange_rate = True jv.flags.ignore_exchange_rate = True
jv.remark = None jv.remark = None
@ -787,9 +821,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
inv.against_voucher, inv.against_voucher,
None, None,
inv.cost_center, inv.cost_center,
dimensions_dict,
) )
@erpnext.allow_regional @erpnext.allow_regional
def adjust_allocations_for_taxes(doc): def adjust_allocations_for_taxes(doc):
pass pass
@frappe.whitelist()
def get_queries_for_dimension_filters(company: str = None):
dimensions_with_filters = []
for d in get_dimensions()[0]:
filters = {}
meta = frappe.get_meta(d.document_type)
if meta.has_field("company") and company:
filters.update({"company": company})
if meta.is_tree:
filters.update({"is_group": 0})
dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters})
return dimensions_with_filters

View File

@ -24,7 +24,9 @@
"difference_account", "difference_account",
"exchange_rate", "exchange_rate",
"currency", "currency",
"cost_center" "accounting_dimensions_section",
"cost_center",
"dimension_col_break"
], ],
"fields": [ "fields": [
{ {
@ -157,12 +159,21 @@
"fieldname": "gain_loss_posting_date", "fieldname": "gain_loss_posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Difference Posting Date" "label": "Difference Posting Date"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"is_virtual": 1, "is_virtual": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-17 17:33:38.612615", "modified": "2023-12-14 13:38:26.104150",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Allocation", "name": "Payment Reconciliation Allocation",

View File

@ -169,6 +169,13 @@ class PaymentRequest(Document):
elif self.payment_channel == "Phone": elif self.payment_channel == "Phone":
self.request_phone_payment() self.request_phone_payment()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
def request_phone_payment(self): def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway) controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount() request_amount = self.get_request_amount()
@ -207,6 +214,14 @@ class PaymentRequest(Document):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
self.set_as_cancelled() self.set_as_cancelled()
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
def make_invoice(self): def make_invoice(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart": if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":

View File

@ -371,7 +371,7 @@ class POSInvoice(SalesInvoice):
if d.get("qty") > 0: if d.get("qty") > 0:
frappe.throw( frappe.throw(
_( _(
"Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return." "Row #{}: You cannot add positive quantities in a return invoice. Please remove item {} to complete the return."
).format(d.idx, frappe.bold(d.item_code)), ).format(d.idx, frappe.bold(d.item_code)),
title=_("Invalid Item"), title=_("Invalid Item"),
) )

View File

@ -132,7 +132,7 @@ class POSProfile(Document):
if len(customer_groups) != len(set(customer_groups)): if len(customer_groups) != len(set(customer_groups)):
frappe.throw( frappe.throw(
_("Duplicate customer group found in the cutomer group table"), _("Duplicate customer group found in the customer group table"),
title=_("Duplicate Customer Group"), title=_("Duplicate Customer Group"),
) )

View File

@ -339,7 +339,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"fieldname": "col_break1", "fieldname": "col_break1",
@ -608,7 +608,7 @@
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2023-02-14 04:53:34.887358", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@ -232,7 +232,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"fieldname": "column_break_26", "fieldname": "column_break_26",
@ -278,7 +278,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-05-06 16:20:22.039078", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Promotional Scheme", "name": "Promotional Scheme",

View File

@ -64,6 +64,7 @@
"warehouse", "warehouse",
"from_warehouse", "from_warehouse",
"quality_inspection", "quality_inspection",
"add_serial_batch_bundle",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"serial_no", "serial_no",
"col_br_wh", "col_br_wh",
@ -913,12 +914,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "WIP Composite Asset", "label": "WIP Composite Asset",
"options": "Asset" "options": "Asset"
},
{
"depends_on": "eval:parent.update_stock === 1",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-12-25 22:00:28.043555", "modified": "2024-01-21 19:46:25.537861",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -51,7 +51,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", "options": "\nTrialing\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1 "read_only": 1
}, },
{ {
@ -267,7 +267,7 @@
"link_fieldname": "subscription" "link_fieldname": "subscription"
} }
], ],
"modified": "2023-12-28 17:20:42.687789", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription", "name": "Subscription",

View File

@ -78,9 +78,7 @@ class Subscription(Document):
purchase_tax_template: DF.Link | None purchase_tax_template: DF.Link | None
sales_tax_template: DF.Link | None sales_tax_template: DF.Link | None
start_date: DF.Date | None start_date: DF.Date | None
status: DF.Literal[ status: DF.Literal["", "Trialing", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"]
"", "Trialling", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"
]
submit_invoice: DF.Check submit_invoice: DF.Check
trial_period_end: DF.Date | None trial_period_end: DF.Date | None
trial_period_start: DF.Date | None trial_period_start: DF.Date | None
@ -233,7 +231,7 @@ class Subscription(Document):
Sets the status of the `Subscription` Sets the status of the `Subscription`
""" """
if self.is_trialling(): if self.is_trialling():
self.status = "Trialling" self.status = "Trialing"
elif ( elif (
self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date) self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date)
): ):

View File

@ -1,7 +1,7 @@
frappe.listview_settings['Subscription'] = { frappe.listview_settings['Subscription'] = {
get_indicator: function(doc) { get_indicator: function(doc) {
if(doc.status === 'Trialling') { if(doc.status === 'Trialing') {
return [__("Trialling"), "green"]; return [__("Trialing"), "green"];
} else if(doc.status === 'Active') { } else if(doc.status === 'Active') {
return [__("Active"), "green"]; return [__("Active"), "green"];
} else if(doc.status === 'Completed') { } else if(doc.status === 'Completed') {

View File

@ -46,7 +46,7 @@ class TestSubscription(FrappeTestCase):
get_date_str(subscription.current_invoice_end), get_date_str(subscription.current_invoice_end),
) )
self.assertEqual(subscription.invoices, []) self.assertEqual(subscription.invoices, [])
self.assertEqual(subscription.status, "Trialling") self.assertEqual(subscription.status, "Trialing")
def test_create_subscription_without_trial_with_correct_period(self): def test_create_subscription_without_trial_with_correct_period(self):
subscription = create_subscription() subscription = create_subscription()

View File

@ -4,7 +4,7 @@
"doctype": "Form Tour", "doctype": "Form Tour",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"modified": "2021-06-29 17:00:26.145996", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",
@ -82,7 +82,7 @@
"label": "Accounts Frozen Till Date", "label": "Accounts Frozen Till Date",
"parent_field": "", "parent_field": "",
"position": "Right", "position": "Right",
"title": "Accounts Frozen Upto" "title": "Accounts Frozen Up To"
}, },
{ {
"description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.", "description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.",

View File

@ -39,7 +39,7 @@ frappe.query_reports["Account Balance"] = {
{ "value": "Asset Received But Not Billed", "label": __("Asset Received But Not Billed") }, { "value": "Asset Received But Not Billed", "label": __("Asset Received But Not Billed") },
{ "value": "Bank", "label": __("Bank") }, { "value": "Bank", "label": __("Bank") },
{ "value": "Cash", "label": __("Cash") }, { "value": "Cash", "label": __("Cash") },
{ "value": "Chargeble", "label": __("Chargeble") }, { "value": "Chargeable", "label": __("Chargeable") },
{ "value": "Capital Work in Progress", "label": __("Capital Work in Progress") }, { "value": "Capital Work in Progress", "label": __("Capital Work in Progress") },
{ "value": "Cost of Goods Sold", "label": __("Cost of Goods Sold") }, { "value": "Cost of Goods Sold", "label": __("Cost of Goods Sold") },
{ "value": "Depreciation", "label": __("Depreciation") }, { "value": "Depreciation", "label": __("Depreciation") },

View File

@ -10,10 +10,8 @@
<h2 class="text-center" style="margin-top:0">{%= __(report.report_name) %}</h2> <h2 class="text-center" style="margin-top:0">{%= __(report.report_name) %}</h2>
<h4 class="text-center"> <h4 class="text-center">
{% if (filters.customer_name) { %} {% if (filters.party) { %}
{%= filters.customer_name %} {%= __(filters.party) %}
{% } else { %}
{%= filters.customer || filters.supplier %}
{% } %} {% } %}
</h4> </h4>
<h6 class="text-center"> <h6 class="text-center">
@ -141,7 +139,7 @@
<th style="width: 24%">{%= __("Reference") %}</th> <th style="width: 24%">{%= __("Reference") %}</th>
{% } %} {% } %}
{% if(!filters.show_future_payments) { %} {% if(!filters.show_future_payments) { %}
<th style="width: 20%">{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}</th> <th style="width: 20%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
{% } %} {% } %}
<th style="width: 10%; text-align: right">{%= __("Invoiced Amount") %}</th> <th style="width: 10%; text-align: right">{%= __("Invoiced Amount") %}</th>
{% if(!filters.show_future_payments) { %} {% if(!filters.show_future_payments) { %}
@ -158,7 +156,7 @@
<th style="width: 10%">{%= __("Remaining Balance") %}</th> <th style="width: 10%">{%= __("Remaining Balance") %}</th>
{% } %} {% } %}
{% } else { %} {% } else { %}
<th style="width: 40%">{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}</th> <th style="width: 40%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
<th style="width: 15%">{%= __("Total Invoiced Amount") %}</th> <th style="width: 15%">{%= __("Total Invoiced Amount") %}</th>
<th style="width: 15%">{%= __("Total Paid Amount") %}</th> <th style="width: 15%">{%= __("Total Paid Amount") %}</th>
<th style="width: 15%">{%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %}</th> <th style="width: 15%">{%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %}</th>
@ -187,7 +185,7 @@
{% if(!filters.show_future_payments) { %} {% if(!filters.show_future_payments) { %}
<td> <td>
{% if(!(filters.customer || filters.supplier)) { %} {% if(!(filters.party)) { %}
{%= data[i]["party"] %} {%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %} <br> {%= data[i]["customer_name"] %}
@ -260,7 +258,7 @@
{% if(data[i]["party"]|| "&nbsp;") { %} {% if(data[i]["party"]|| "&nbsp;") { %}
{% if(!data[i]["is_total_row"]) { %} {% if(!data[i]["is_total_row"]) { %}
<td> <td>
{% if(!(filters.customer || filters.supplier)) { %} {% if(!(filters.party)) { %}
{%= data[i]["party"] %} {%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %} <br> {%= data[i]["customer_name"] %}

View File

@ -8,17 +8,7 @@ import re
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import ( from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
add_days,
add_months,
cint,
cstr,
flt,
formatdate,
get_first_day,
getdate,
today,
)
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@ -53,8 +43,6 @@ def get_period_list(
year_start_date = getdate(period_start_date) year_start_date = getdate(period_start_date)
year_end_date = getdate(period_end_date) year_end_date = getdate(period_end_date)
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
period_list = [] period_list = []

View File

@ -46,12 +46,10 @@ def get_result(
out = [] out = []
for name, details in gle_map.items(): for name, details in gle_map.items():
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
bill_no, bill_date = "", ""
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
for entry in details: for entry in details:
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
tax_withholding_category, rate = None, None
bill_no, bill_date = "", ""
party = entry.party or entry.against party = entry.party or entry.against
posting_date = entry.posting_date posting_date = entry.posting_date
voucher_type = entry.voucher_type voucher_type = entry.voucher_type
@ -61,13 +59,20 @@ def get_result(
if party_list: if party_list:
party = party_list[0] party = party_list[0]
if entry.account in tds_accounts.keys():
tax_amount += entry.credit - entry.debit
# infer tax withholding category from the account if it's the single account for this category
tax_withholding_category = tds_accounts.get(entry.account)
rate = tax_rate_map.get(tax_withholding_category)
# or else the consolidated value from the voucher document
if not tax_withholding_category:
# or else from the party default
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
if not tax_withholding_category: if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category) rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
tax_amount += entry.credit - entry.debit
if net_total_map.get(name): if net_total_map.get(name):
if voucher_type == "Journal Entry" and tax_amount and rate: if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount # back calcalute total amount from rate and tax_amount
@ -282,11 +287,20 @@ def get_tds_docs(filters):
journal_entry_party_map = frappe._dict() journal_entry_party_map = frappe._dict()
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
tds_accounts = frappe.get_all( _tds_accounts = frappe.get_all(
"Tax Withholding Account", {"company": filters.get("company")}, pluck="account" "Tax Withholding Account",
{"company": filters.get("company")},
["account", "parent"],
) )
tds_accounts = {}
for tds_acc in _tds_accounts:
# if it turns out not to be the only tax withholding category, then don't include in the map
if tds_accounts.get(tds_acc["account"]):
tds_accounts[tds_acc["account"]] = None
else:
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True) tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
for d in tds_docs: for d in tds_docs:
if d.voucher_type == "Purchase Invoice": if d.voucher_type == "Purchase Invoice":

View File

@ -453,7 +453,19 @@ def add_cc(args=None):
return cc.name return cc.name
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep def _build_dimensions_dict_for_exc_gain_loss(
entry: dict | object = None, active_dimensions: list = None
):
dimensions_dict = frappe._dict()
if entry and active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
return dimensions_dict
def reconcile_against_document(
args, skip_ref_details_update_for_pe=False, active_dimensions=None
): # nosemgrep
""" """
Cancel PE or JV, Update against document, split if required and resubmit Cancel PE or JV, Update against document, split if required and resubmit
""" """
@ -482,6 +494,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
check_if_advance_entry_modified(entry) check_if_advance_entry_modified(entry)
validate_allocated_amount(entry) validate_allocated_amount(entry)
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
# update ref in advance entry # update ref in advance entry
if voucher_type == "Journal Entry": if voucher_type == "Journal Entry":
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
@ -489,10 +503,14 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
# amount and account in args # amount and account in args
# referenced_row is used to deduplicate gain/loss journal # referenced_row is used to deduplicate gain/loss journal
entry.update({"referenced_row": referenced_row}) entry.update({"referenced_row": referenced_row})
doc.make_exchange_gain_loss_journal([entry]) doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
else: else:
referenced_row = update_reference_in_payment_entry( referenced_row = update_reference_in_payment_entry(
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe entry,
doc,
do_not_save=True,
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
) )
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
@ -600,7 +618,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked # Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"): advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if jv_detail.get("reference_type") in advance_payment_doctypes:
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid() frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
@ -654,7 +675,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
def update_reference_in_payment_entry( def update_reference_in_payment_entry(
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None
): ):
reference_details = { reference_details = {
"reference_doctype": d.against_voucher_type, "reference_doctype": d.against_voucher_type,
@ -667,13 +688,17 @@ def update_reference_in_payment_entry(
else payment_entry.get_exchange_rate(), else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.difference_amount, "exchange_gain_loss": d.difference_amount,
"account": d.account, "account": d.account,
"dimensions": d.dimensions,
} }
if d.voucher_detail_no: if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked # Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"): advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if existing_row.get("reference_doctype") in advance_payment_doctypes:
frappe.get_doc( frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid() ).set_total_advance_paid()
@ -699,8 +724,9 @@ def update_reference_in_payment_entry(
if not skip_ref_details_update_for_pe: if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details() payment_entry.set_missing_ref_details()
payment_entry.set_amounts() payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal( payment_entry.make_exchange_gain_loss_journal(
frappe._dict({"difference_posting_date": d.difference_posting_date}) frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
) )
if not do_not_save: if not do_not_save:
@ -2042,6 +2068,7 @@ def create_gain_loss_journal(
ref2_dn, ref2_dn,
ref2_detail_no, ref2_detail_no,
cost_center, cost_center,
dimensions,
) -> str: ) -> str:
journal_entry = frappe.new_doc("Journal Entry") journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.voucher_type = "Exchange Gain Or Loss"
@ -2075,7 +2102,8 @@ def create_gain_loss_journal(
dr_or_cr + "_in_account_currency": 0, dr_or_cr + "_in_account_currency": 0,
} }
) )
if dimensions:
journal_account.update(dimensions)
journal_entry.append("accounts", journal_account) journal_entry.append("accounts", journal_account)
journal_account = frappe._dict( journal_account = frappe._dict(
@ -2091,7 +2119,8 @@ def create_gain_loss_journal(
reverse_dr_or_cr: abs(exc_gain_loss), reverse_dr_or_cr: abs(exc_gain_loss),
} }
) )
if dimensions:
journal_account.update(dimensions)
journal_entry.append("accounts", journal_account) journal_entry.append("accounts", journal_account)
journal_entry.save() journal_entry.save()

View File

@ -519,13 +519,10 @@ class Asset(AccountsController):
movement.cancel() movement.cancel()
def cancel_capitalization(self): def cancel_capitalization(self):
asset_capitalization = frappe.db.get_value( if self.capitalized_in:
"Asset Capitalization", self.db_set("capitalized_in", None)
{"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, asset_capitalization = frappe.get_doc("Asset Capitalization", self.capitalized_in)
) if asset_capitalization.docstatus == 1:
if asset_capitalization:
asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
asset_capitalization.cancel() asset_capitalization.cancel()
def delete_depreciation_entries(self): def delete_depreciation_entries(self):

View File

@ -561,6 +561,8 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
def reverse_depreciation_entry_made_after_disposal(asset, date): def reverse_depreciation_entry_made_after_disposal(asset, date):
for row in asset.get("finance_books"): for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
if not asset_depr_schedule_doc:
continue
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date: if schedule.schedule_date == date:

View File

@ -146,6 +146,7 @@ class AssetCapitalization(StockController):
def cancel_target_asset(self): def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset: if self.entry_type == "Capitalization" and self.target_asset:
asset_doc = frappe.get_doc("Asset", self.target_asset) asset_doc = frappe.get_doc("Asset", self.target_asset)
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
if asset_doc.docstatus == 1: if asset_doc.docstatus == 1:
asset_doc.cancel() asset_doc.cancel()

View File

@ -40,7 +40,7 @@ class AssetMaintenance(Document):
if getdate(task.next_due_date) < getdate(nowdate()): if getdate(task.next_due_date) < getdate(nowdate()):
task.maintenance_status = "Overdue" task.maintenance_status = "Overdue"
if not task.assign_to and self.docstatus == 0: if not task.assign_to and self.docstatus == 0:
throw(_("Row #{}: Please asign task to a member.").format(task.idx)) throw(_("Row #{}: Please assign task to a member.").format(task.idx))
def on_update(self): def on_update(self):
for task in self.get("asset_maintenance_tasks"): for task in self.get("asset_maintenance_tasks"):

View File

@ -2,14 +2,14 @@
"action": "Show Form Tour", "action": "Show Form Tour",
"action_label": "Let's review existing Asset Category", "action_label": "Let's review existing Asset Category",
"creation": "2021-08-13 14:26:18.656303", "creation": "2021-08-13 14:26:18.656303",
"description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipments\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n", "description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipment\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-11-23 10:02:03.242127", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Asset Category", "name": "Asset Category",
"owner": "Administrator", "owner": "Administrator",

View File

@ -202,7 +202,7 @@ def prepare_chart_data(data, filters):
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()], "values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
}, },
{ {
"name": _("Depreciatied Amount"), "name": _("Depreciated Amount"),
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()], "values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
}, },
], ],

View File

@ -134,6 +134,7 @@
"more_info_tab", "more_info_tab",
"tracking_section", "tracking_section",
"status", "status",
"advance_payment_status",
"column_break_75", "column_break_75",
"per_billed", "per_billed",
"per_received", "per_received",
@ -1269,13 +1270,25 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Connections", "label": "Connections",
"show_dashboard": 1 "show_dashboard": 1
},
{
"fieldname": "advance_payment_status",
"fieldtype": "Select",
"hidden": 1,
"in_standard_filter": 1,
"label": "Advance Payment Status",
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid",
"print_hide": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-10-01 20:58:07.851037", "modified": "2023-10-10 13:37:40.158761",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -215,6 +215,10 @@ class PurchaseOrder(BuyingController):
self.validate_fg_item_for_subcontracting() self.validate_fg_item_for_subcontracting()
self.set_received_qty_for_drop_ship_items() self.set_received_qty_for_drop_ship_items()
if not self.advance_payment_status:
self.advance_payment_status = "Not Initiated"
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_order_reference self.doctype, self.supplier, self.company, self.inter_company_order_reference
) )

View File

@ -1,6 +1,6 @@
frappe.listview_settings['Purchase Order'] = { frappe.listview_settings['Purchase Order'] = {
add_fields: ["base_grand_total", "company", "currency", "supplier", add_fields: ["base_grand_total", "company", "currency", "supplier",
"supplier_name", "per_received", "per_billed", "status"], "supplier_name", "per_received", "per_billed", "status", "advance_payment_status"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status === "Closed") { if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"]; return [__("Closed"), "green", "status,=,Closed"];
@ -8,6 +8,8 @@ frappe.listview_settings['Purchase Order'] = {
return [__("On Hold"), "orange", "status,=,On Hold"]; return [__("On Hold"), "orange", "status,=,On Hold"];
} else if (doc.status === "Delivered") { } else if (doc.status === "Delivered") {
return [__("Delivered"), "green", "status,=,Closed"]; return [__("Delivered"), "green", "status,=,Closed"];
} else if (doc.advance_payment_status == "Initiated") {
return [__("To Pay"), "gray", "advance_payment_status,=,Initiated"];
} else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") { } else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") {
if (flt(doc.per_billed, 2) < 100) { if (flt(doc.per_billed, 2) < 100) {
return [__("To Receive and Bill"), "orange", return [__("To Receive and Bill"), "orange",

View File

@ -1021,6 +1021,33 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name})) self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name}))
def test_purchase_order_advance_payment_status(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
po = create_purchase_order()
self.assertEqual(
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
)
pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True)
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
pe = get_payment_entry(po.doctype, po.name).save().submit()
self.assertEqual(
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid"
)
pe.reload()
pe.cancel()
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
pr.reload()
pr.cancel()
self.assertEqual(
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@ -54,7 +54,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
"width": "80", "width": "80",
get_data: function(txt) { get_data: function(txt) {
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"] let status = ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"]
let options = [] let options = []
for (let option of status){ for (let option of status){
options.push({ options.push({

View File

@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _, bold, qb, throw from frappe import _, bold, qb, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum from frappe.query_builder.functions import Abs, Sum
from frappe.utils import ( from frappe.utils import (
@ -21,12 +22,14 @@ from frappe.utils import (
get_link_to_form, get_link_to_form,
getdate, getdate,
nowdate, nowdate,
parse_json,
today, today,
) )
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
get_dimensions,
) )
from erpnext.accounts.doctype.pricing_rule.utils import ( from erpnext.accounts.doctype.pricing_rule.utils import (
apply_pricing_rule_for_free_items, apply_pricing_rule_for_free_items,
@ -831,6 +834,37 @@ class AccountsController(TransactionBase):
self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges"))) self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges")))
def append_taxes_from_item_tax_template(self):
if not frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
return
for row in self.items:
item_tax_rate = row.get("item_tax_rate")
if not item_tax_rate:
continue
if isinstance(item_tax_rate, str):
item_tax_rate = parse_json(item_tax_rate)
for account_head, rate in item_tax_rate.items():
row = self.get_tax_row(account_head)
if not row:
self.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": account_head,
"rate": 0,
"description": account_head,
},
)
def get_tax_row(self, account_head):
for row in self.taxes:
if row.account_head == account_head:
return row
def set_other_charges(self): def set_other_charges(self):
self.set("taxes", []) self.set("taxes", [])
self.set_taxes() self.set_taxes()
@ -1216,7 +1250,9 @@ class AccountsController(TransactionBase):
return True return True
return False return False
def make_exchange_gain_loss_journal(self, args: dict = None) -> None: def make_exchange_gain_loss_journal(
self, args: dict = None, dimensions_dict: dict = None
) -> None:
""" """
Make Exchange Gain/Loss journal for Invoices and Payments Make Exchange Gain/Loss journal for Invoices and Payments
""" """
@ -1271,6 +1307,7 @@ class AccountsController(TransactionBase):
self.name, self.name,
arg.get("referenced_row"), arg.get("referenced_row"),
arg.get("cost_center"), arg.get("cost_center"),
dimensions_dict,
) )
frappe.msgprint( frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format( _("Exchange Gain/Loss amount has been booked through {0}").format(
@ -1351,6 +1388,7 @@ class AccountsController(TransactionBase):
self.name, self.name,
d.idx, d.idx,
self.cost_center, self.cost_center,
dimensions_dict,
) )
frappe.msgprint( frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format( _("Exchange Gain/Loss amount has been booked through {0}").format(
@ -1415,7 +1453,13 @@ class AccountsController(TransactionBase):
if lst: if lst:
from erpnext.accounts.utils import reconcile_against_document from erpnext.accounts.utils import reconcile_against_document
reconcile_against_document(lst) # pass dimension values to utility method
active_dimensions = get_dimensions()[0]
for x in lst:
for dim in active_dimensions:
if self.get(dim.fieldname):
x.update({dim.fieldname: self.get(dim.fieldname)})
reconcile_against_document(lst, active_dimensions=active_dimensions)
def on_cancel(self): def on_cancel(self):
from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
@ -1749,7 +1793,10 @@ class AccountsController(TransactionBase):
def set_total_advance_paid(self): def set_total_advance_paid(self):
ple = frappe.qb.DocType("Payment Ledger Entry") ple = frappe.qb.DocType("Payment Ledger Entry")
party = self.customer if self.doctype == "Sales Order" else self.supplier if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
party = self.customer
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
party = self.supplier
advance = ( advance = (
frappe.qb.from_(ple) frappe.qb.from_(ple)
.select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount")) .select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount"))
@ -1763,6 +1810,8 @@ class AccountsController(TransactionBase):
.run(as_dict=True) .run(as_dict=True)
) )
advance_paid, order_total = None, None
if advance: if advance:
advance = advance[0] advance = advance[0]
@ -1791,7 +1840,38 @@ class AccountsController(TransactionBase):
).format(formatted_advance_paid, self.name, formatted_order_total) ).format(formatted_advance_paid, self.name, formatted_order_total)
) )
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid) self.db_set("advance_paid", advance_paid)
self.set_advance_payment_status(advance_paid, order_total)
def set_advance_payment_status(
self, advance_paid: float | None = None, order_total: float | None = None
):
new_status = None
# if money is paid set the paid states
if advance_paid:
new_status = "Partially Paid" if advance_paid < order_total else "Fully Paid"
if not new_status:
prs = frappe.db.count(
"Payment Request",
{
"reference_doctype": self.doctype,
"reference_name": self.name,
"docstatus": 1,
},
)
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
new_status = "Requested" if prs else "Not Requested"
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
new_status = "Initiated" if prs else "Not Initiated"
if new_status == self.advance_payment_status:
return
self.db_set("advance_payment_status", new_status)
self.set_status(update=True)
self.notify_update()
@property @property
def company_abbr(self): def company_abbr(self):
@ -2684,47 +2764,37 @@ def get_common_query(
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
if condition: if condition:
if condition.get("name", None): # conditions should be built as an array and passed as Criterion
q = q.where(payment_entry.name.like(f"%{condition.get('name')}%")) common_filter_conditions = []
common_filter_conditions.append(payment_entry.company == condition["company"])
if condition.get("name", None):
common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%"))
if condition.get("from_payment_date"):
common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"]))
if condition.get("to_payment_date"):
common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"]))
q = q.where(payment_entry.company == condition["company"])
q = (
q.where(payment_entry.posting_date >= condition["from_payment_date"])
if condition.get("from_payment_date")
else q
)
q = (
q.where(payment_entry.posting_date <= condition["to_payment_date"])
if condition.get("to_payment_date")
else q
)
if condition.get("get_payments") == True: if condition.get("get_payments") == True:
q = ( if condition.get("cost_center"):
q.where(payment_entry.cost_center == condition["cost_center"]) common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])
if condition.get("cost_center")
else q if condition.get("accounting_dimensions"):
for field, val in condition.get("accounting_dimensions").items():
common_filter_conditions.append(payment_entry[field] == val)
if condition.get("minimum_payment_amount"):
common_filter_conditions.append(
payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
) )
q = (
q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"]) if condition.get("maximum_payment_amount"):
if condition.get("minimum_payment_amount") common_filter_conditions.append(
else q payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
)
q = (
q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"])
if condition.get("maximum_payment_amount")
else q
)
else:
q = (
q.where(payment_entry.total_debit >= condition["minimum_payment_amount"])
if condition.get("minimum_payment_amount")
else q
)
q = (
q.where(payment_entry.total_debit <= condition["maximum_payment_amount"])
if condition.get("maximum_payment_amount")
else q
) )
q = q.where(Criterion.all(common_filter_conditions))
q = q.orderby(payment_entry.posting_date) q = q.orderby(payment_entry.posting_date)
q = q.limit(limit) if limit else q q = q.limit(limit) if limit else q

View File

@ -53,6 +53,10 @@ status_map = {
"To Deliver", "To Deliver",
"eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note", "eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note",
], ],
[
"To Pay",
"eval:self.advance_payment_status == 'Requested' and self.docstatus == 1",
],
[ [
"Completed", "Completed",
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1", "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1",
@ -63,15 +67,19 @@ status_map = {
], ],
"Purchase Order": [ "Purchase Order": [
["Draft", None], ["Draft", None],
[
"To Receive and Bill",
"eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1",
],
["To Bill", "eval:self.per_received >= 100 and self.per_billed < 100 and self.docstatus == 1"], ["To Bill", "eval:self.per_received >= 100 and self.per_billed < 100 and self.docstatus == 1"],
[ [
"To Receive", "To Receive",
"eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1", "eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1",
], ],
[
"To Receive and Bill",
"eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1",
],
[
"To Pay",
"eval:self.advance_payment_status == 'Initiated' and self.docstatus == 1",
],
[ [
"Completed", "Completed",
"eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1", "eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1",

View File

@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
20 series - Sales Invoice against Journals 20 series - Sales Invoice against Journals
30 series - Sales Invoice against Credit Notes 30 series - Sales Invoice against Credit Notes
40 series - Company default Cost center is unset 40 series - Company default Cost center is unset
50 series - Dimension inheritence
""" """
def setUp(self): def setUp(self):
@ -1255,3 +1256,214 @@ class TestAccountsController(FrappeTestCase):
) )
frappe.db.set_value("Company", self.company, "cost_center", cc) frappe.db.set_value("Company", self.company, "cost_center", cc)
def setup_dimensions(self):
# create dimension
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
)
create_dimension()
# make it non-mandatory
loc = frappe.get_doc("Accounting Dimension", "Location")
for x in loc.dimension_defaults:
x.mandatory_for_bs = False
x.mandatory_for_pl = False
loc.save()
def test_50_dimensions_filter(self):
"""
Test workings of dimension filters
"""
self.setup_dimensions()
rate_in_account_currency = 1
# Invoices
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si1.department = "Management"
si1.save().submit()
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si2.department = "Operations"
si2.save().submit()
# Payments
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note1.department = "Management"
cr_note1.is_return = 1
cr_note1.save().submit()
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note2.department = "Legal"
cr_note2.is_return = 1
cr_note2.save().submit()
pe1 = get_payment_entry(si1.doctype, si1.name)
pe1.references = []
pe1.department = "Research & Development"
pe1.save().submit()
pe2 = get_payment_entry(si1.doctype, si1.name)
pe2.references = []
pe2.department = "Management"
pe2.save().submit()
je1 = self.create_journal_entry(
acc1=self.debit_usd,
acc1_exc_rate=75,
acc2=self.cash,
acc1_amount=-1,
acc2_amount=-75,
acc2_exc_rate=1,
)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1.accounts[0].department = "Management"
je1.save().submit()
# assert dimension filter's result
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 2)
self.assertEqual(len(pr.payments), 5)
pr.department = "Legal"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
pr.department = "Management"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 3)
pr.department = "Research & Development"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
def test_51_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1
# Invoice
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si.department = "Management"
si.save().submit()
# Payment
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note.department = "Management"
cr_note.is_return = 1
cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.department = "Management"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss)
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 2)
self.assertEqual(len(exc_je_for_cr_note), 2)
self.assertEqual(exc_je_for_si, exc_je_for_cr_note)
for x in exc_je_for_si + exc_je_for_cr_note:
with self.subTest(x=x):
self.assertEqual(
[cr_note.department, cr_note.department],
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
)
def test_52_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate = 80
rate_in_account_currency = 1
dpt = "Research & Development"
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
si.department = dpt
si.save().submit()
pe = self.create_payment_entry(amount=1, source_exc_rate=82).save()
pe.department = dpt
pe = pe.save().submit()
pr = self.create_payment_reconciliation()
pr.department = dpt
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# Exc Gain/Loss journals should inherit dimension from parent
journals = self.get_journals_for(si.doctype, si.name)
self.assertEqual(
[dpt, dpt],
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", [x.parent for x in journals])},
pluck="department",
),
)
def test_53_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
adv.department = dpt
adv.save().submit()
adv.reload()
# Sales Invoices in different exchange rates
si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True)
si.department = dpt
advances = si.get_advance_entries()
self.assertEqual(len(advances), 1)
self.assertEqual(advances[0].reference_name, adv.name)
si.append(
"advances",
{
"doctype": "Sales Invoice Advance",
"reference_type": advances[0].reference_type,
"reference_name": advances[0].reference_name,
"reference_row": advances[0].reference_row,
"advance_amount": 1,
"allocated_amount": 1,
"ref_exchange_rate": advances[0].exchange_rate,
"remarks": advances[0].remarks,
},
)
si = si.save().submit()
# Outstanding in both currencies should be '0'
adv.reload()
self.assertEqual(si.outstanding_amount, 0)
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
# Exc Gain/Loss journals should inherit dimension from parent
journals = self.get_journals_for(si.doctype, si.name)
self.assertEqual(
[dpt, dpt],
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", [x.parent for x in journals])},
pluck="department",
),
)

View File

@ -155,7 +155,7 @@ class TallyMigration(Document):
except RecursionError: except RecursionError:
self.log( self.log(
_( _(
"Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name" "Error occurred while parsing Chart of Accounts: Please make sure that no two accounts have the same name"
) )
) )

View File

@ -481,7 +481,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
communication_doctypes = ["Customer", "Supplier"] communication_doctypes = ["Customer", "Supplier"]
advance_payment_doctypes = ["Sales Order", "Purchase Order"] advance_payment_customer_doctypes = ["Sales Order"]
advance_payment_supplier_doctypes = ["Purchase Order"]
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"] invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
@ -538,6 +539,8 @@ accounting_dimension_doctypes = [
"Account Closing Balance", "Account Closing Balance",
"Supplier Quotation", "Supplier Quotation",
"Supplier Quotation Item", "Supplier Quotation Item",
"Payment Reconciliation",
"Payment Reconciliation Allocation",
] ]
get_matching_queries = ( get_matching_queries = (

View File

@ -222,7 +222,7 @@ class MaintenanceSchedule(TransactionBase):
def validate_maintenance_detail(self): def validate_maintenance_detail(self):
if not self.get("items"): if not self.get("items"):
throw(_("Please enter Maintaince Details first")) throw(_("Please enter Maintenance Details first"))
for d in self.get("items"): for d in self.get("items"):
if not d.item_code: if not d.item_code:

View File

@ -998,12 +998,6 @@ class TestWorkOrder(FrappeTestCase):
make_job_card(wo_order.name, operations) make_job_card(wo_order.name, operations)
job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name, "docstatus": 0}, "name") job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name, "docstatus": 0}, "name")
update_job_card(job_card, 10, 2)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.is_scrap_item:
self.assertEqual(row.qty, 2)
def test_close_work_order(self): def test_close_work_order(self):
items = [ items = [

View File

@ -1520,7 +1520,7 @@ def validate_operation_data(row):
if row.get("qty") > row.get("pending_qty"): if row.get("qty") > row.get("pending_qty"):
frappe.throw( frappe.throw(
_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format( _("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
frappe.bold(row.get("operation")), frappe.bold(row.get("operation")),
frappe.bold(row.get("qty")), frappe.bold(row.get("qty")),
frappe.bold(row.get("pending_qty")), frappe.bold(row.get("pending_qty")),

View File

@ -352,6 +352,7 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System Settings", "date_format")) execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System Settings", "date_format"))
erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v15_0.create_advance_payment_status
# below migration patch should always run last # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20

View File

@ -188,4 +188,4 @@ def execute():
raise err raise err
else: else:
break break
print(f"{processed} records have been sucessfully migrated") print(f"{processed} records have been successfully migrated")

View File

@ -0,0 +1,54 @@
import frappe
def execute():
"""
Description:
Calculate the new Advance Payment Statuse column in SO & PO
"""
if frappe.reload_doc("selling", "doctype", "Sales Order"):
so = frappe.qb.DocType("Sales Order")
frappe.qb.update(so).set(so.advance_payment_status, "Not Requested").where(
so.docstatus == 1
).where(so.advance_paid == 0.0).run()
frappe.qb.update(so).set(so.advance_payment_status, "Partially Paid").where(
so.docstatus == 1
).where(so.advance_payment_status.isnull()).where(
so.advance_paid < (so.rounded_total or so.grand_total)
).run()
frappe.qb.update(so).set(so.advance_payment_status, "Fully Paid").where(so.docstatus == 1).where(
so.advance_payment_status.isnull()
).where(so.advance_paid == (so.rounded_total or so.grand_total)).run()
pr = frappe.qb.DocType("Payment Request")
frappe.qb.update(so).join(pr).on(so.name == pr.reference_name).set(
so.advance_payment_status, "Requested"
).where(so.docstatus == 1).where(pr.docstatus == 1).where(
so.advance_payment_status == "Not Requested"
).run()
if frappe.reload_doc("buying", "doctype", "Purchase Order"):
po = frappe.qb.DocType("Purchase Order")
frappe.qb.update(po).set(po.advance_payment_status, "Not Initiated").where(
po.docstatus == 1
).where(po.advance_paid == 0.0).run()
frappe.qb.update(po).set(po.advance_payment_status, "Partially Paid").where(
po.docstatus == 1
).where(po.advance_payment_status.isnull()).where(
po.advance_paid < (po.rounded_total or po.grand_total)
).run()
frappe.qb.update(po).set(po.advance_payment_status, "Fully Paid").where(po.docstatus == 1).where(
po.advance_payment_status.isnull()
).where(po.advance_paid == (po.rounded_total or po.grand_total)).run()
pr = frappe.qb.DocType("Payment Request")
frappe.qb.update(po).join(pr).on(po.name == pr.reference_name).set(
po.advance_payment_status, "Initiated"
).where(po.docstatus == 1).where(pr.docstatus == 1).where(
po.advance_payment_status == "Not Initiated"
).run()

View File

@ -105,11 +105,27 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false; this.frm.has_items = false;
} }
if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) { if (serial_no) {
this.is_duplicate_serial_no(row, item_code, serial_no)
.then((is_duplicate) => {
if (!is_duplicate) {
this.run_serially_tasks(row, data, resolve);
} else {
this.clean_up(); this.clean_up();
reject(); reject();
return; return;
} }
});
} else {
this.run_serially_tasks(row, data, resolve);
}
});
}
run_serially_tasks(row, data, resolve) {
const {item_code, barcode, batch_no, serial_no, uom} = data;
frappe.run_serially([ frappe.run_serially([
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no), () => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
@ -119,16 +135,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}), }),
() => this.set_barcode_uom(row, uom), () => this.set_barcode_uom(row, uom),
() => this.clean_up(), () => this.clean_up(),
() => resolve(row),
() => { () => {
if (row.serial_and_batch_bundle && !this.frm.is_new()) { if (row.serial_and_batch_bundle && !this.frm.is_new()) {
this.frm.save(); this.frm.save();
} }
frappe.flags.trigger_from_barcode_scanner = false; frappe.flags.trigger_from_barcode_scanner = false;
} },
() => resolve(row),
]); ]);
});
} }
set_item(row, item_code, barcode, batch_no, serial_no) { set_item(row, item_code, barcode, batch_no, serial_no) {
@ -475,26 +490,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
} }
} }
is_duplicate_serial_no(row, item_code, serial_no) { async is_duplicate_serial_no(row, item_code, serial_no) {
let is_duplicate = false;
const promise = new Promise((resolve, reject) => {
if (this.frm.is_new() || !row.serial_and_batch_bundle) { if (this.frm.is_new() || !row.serial_and_batch_bundle) {
let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no); is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
if (is_duplicate) { if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
} }
return is_duplicate; resolve(is_duplicate);
} else if (row.serial_and_batch_bundle) { } else if (row.serial_and_batch_bundle) {
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => { this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
if (r.message) { if (r.message) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
} }
return r.message; is_duplicate = r.message;
resolve(is_duplicate);
}) })
} }
});
return await promise;
} }
async check_duplicate_serial_no_in_db(row, serial_no, response) { check_duplicate_serial_no_in_db(row, serial_no, response) {
frappe.call({ frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no", method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
args: { args: {
@ -504,7 +525,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
callback(r) { callback(r) {
response(r); response(r);
} }
}) });
} }
check_duplicate_serial_no_in_localstorage(item_code, serial_no) { check_duplicate_serial_no_in_localstorage(item_code, serial_no) {

View File

@ -25,6 +25,10 @@ erpnext.accounts.dimensions = {
}, },
setup_filters(frm, doctype) { setup_filters(frm, doctype) {
if (doctype == 'Payment Entry' && this.accounting_dimensions) {
frm.dimension_filters = this.accounting_dimensions
}
if (this.accounting_dimensions) { if (this.accounting_dimensions) {
this.accounting_dimensions.forEach((dimension) => { this.accounting_dimensions.forEach((dimension) => {
frappe.model.with_doctype(dimension['document_type'], () => { frappe.model.with_doctype(dimension['document_type'], () => {

View File

@ -22,6 +22,15 @@ erpnext.sales_common = {
} }
}; };
}); });
this.frm.set_query('project', function(doc) {
return {
query: "erpnext.controllers.queries.get_project_name",
filters: {
'customer': doc.customer
}
}
});
} }
setup_queries() { setup_queries() {

View File

@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
let warehouse = this.item?.type_of_transaction === "Outward" ? let warehouse = this.item?.type_of_transaction === "Outward" ?
(this.item.warehouse || this.item.s_warehouse) : ""; (this.item.warehouse || this.item.s_warehouse) : "";
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
warehouse = this.get_warehouse();
}
return { return {
'item_code': this.item.item_code, 'item_code': this.item.item_code,
'warehouse': ["=", warehouse] 'warehouse': ["=", warehouse]
@ -135,7 +139,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
filters: this.get_serial_no_filters() filters: this.get_serial_no_filters()
}; };
}, },
onchange: () => this.update_serial_batch_no() onchange: () => this.scan_barcode_data()
}); });
} }
@ -145,7 +149,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
options: 'Barcode', options: 'Barcode',
fieldname: 'scan_batch_no', fieldname: 'scan_batch_no',
label: __('Scan Batch No'), label: __('Scan Batch No'),
onchange: () => this.update_serial_batch_no() onchange: () => this.scan_barcode_data()
}); });
} }
@ -179,11 +183,54 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
label = __('Serial Nos / Batch Nos'); label = __('Serial Nos / Batch Nos');
} }
return [ let fields = [
{ {
fieldtype: 'Section Break', fieldtype: 'Section Break',
label: __('{0} {1} via CSV File', [primary_label, label]) label: __('{0} {1} via CSV File', [primary_label, label])
}
]
if (this.item?.has_serial_no) {
fields = [...fields,
{
fieldtype: 'Check',
label: __('Import Using CSV file'),
fieldname: 'import_using_csv_file',
default: 0,
}, },
{
fieldtype: 'Section Break',
label: __('{0} {1} Manually', [primary_label, label]),
depends_on: 'eval:doc.import_using_csv_file === 0',
},
{
fieldtype: 'Small Text',
label: __('Enter Serial Nos'),
fieldname: 'upload_serial_nos',
depends_on: 'eval:doc.import_using_csv_file === 0',
description: __('Enter each serial no in a new line'),
},
{
fieldtype: 'Column Break',
depends_on: 'eval:doc.import_using_csv_file === 0',
},
{
fieldtype: 'Button',
fieldname: 'make_serial_nos',
label: __('Create Serial Nos'),
depends_on: 'eval:doc.import_using_csv_file === 0',
click: () => {
this.create_serial_nos();
}
},
{
fieldtype: 'Section Break',
depends_on: 'eval:doc.import_using_csv_file === 1',
}
];
}
fields = [...fields,
{ {
fieldtype: 'Button', fieldtype: 'Button',
fieldname: 'download_csv', fieldname: 'download_csv',
@ -199,7 +246,32 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
label: __('Attach CSV File'), label: __('Attach CSV File'),
onchange: () => this.upload_csv_file() onchange: () => this.upload_csv_file()
} }
] ];
return fields;
}
create_serial_nos() {
let {upload_serial_nos} = this.dialog.get_values();
if (!upload_serial_nos) {
frappe.throw(__('Please enter Serial Nos'));
}
frappe.call({
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.create_serial_nos',
args: {
item_code: this.item.item_code,
serial_nos: upload_serial_nos
},
callback: (r) => {
if (r.message) {
this.dialog.fields_dict.entries.df.data = [];
this.set_data(r.message);
this.update_bundle_entries();
}
}
});
} }
download_csv_file() { download_csv_file() {
@ -374,6 +446,26 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
} }
} }
scan_barcode_data() {
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
if (scan_serial_no || scan_batch_no) {
frappe.call({
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists',
args: {
item_code: this.item.item_code,
type_of_transaction: this.item.type_of_transaction,
serial_no: scan_serial_no,
batch_no: scan_batch_no,
},
callback: (r) => {
this.update_serial_batch_no();
}
})
}
}
update_serial_batch_no() { update_serial_batch_no() {
const { scan_serial_no, scan_batch_no } = this.dialog.get_values(); const { scan_serial_no, scan_batch_no } = this.dialog.get_values();

View File

@ -64,7 +64,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto", "label": "Valid Up To",
"reqd": 1 "reqd": 1
}, },
{ {
@ -135,7 +135,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-04-18 08:25:35.302081", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "Lower Deduction Certificate", "name": "Lower Deduction Certificate",

View File

@ -37,7 +37,7 @@ class LowerDeductionCertificate(Document):
def validate_dates(self): def validate_dates(self):
if getdate(self.valid_upto) < getdate(self.valid_from): if getdate(self.valid_upto) < getdate(self.valid_from):
frappe.throw(_("Valid Upto date cannot be before Valid From date")) frappe.throw(_("Valid Up To date cannot be before Valid From date"))
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
@ -45,7 +45,7 @@ class LowerDeductionCertificate(Document):
frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date): if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date):
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) frappe.throw(_("Valid Up To date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
def validate_supplier_against_tax_category(self): def validate_supplier_against_tax_category(self):
duplicate_certificate = frappe.db.get_value( duplicate_certificate = frappe.db.get_value(

View File

@ -427,11 +427,11 @@ def create_internal_customer(
if not allowed_to_interact_with: if not allowed_to_interact_with:
allowed_to_interact_with = represents_company allowed_to_interact_with = represents_company
exisiting_representative = frappe.db.get_value( existing_representative = frappe.db.get_value(
"Customer", {"represents_company": represents_company} "Customer", {"represents_company": represents_company}
) )
if exisiting_representative: if existing_representative:
return exisiting_representative return existing_representative
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc( customer = frappe.get_doc(

View File

@ -127,7 +127,8 @@ class Quotation(SellingController):
def validate(self): def validate(self):
super(Quotation, self).validate() super(Quotation, self).validate()
self.set_status() self.set_status()
self.validate_uom_is_integer("stock_uom", "qty") self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty")
self.validate_valid_till() self.validate_valid_till()
self.set_customer_name() self.set_customer_name()
if self.items: if self.items:

View File

@ -593,6 +593,77 @@ class TestQuotation(FrappeTestCase):
quotation.reload() quotation.reload()
self.assertEqual(quotation.status, "Ordered") self.assertEqual(quotation.status, "Ordered")
def test_uom_validation(self):
from erpnext.stock.doctype.item.test_item import make_item
item = "_Test Item FOR UOM Validation"
make_item(item, {"is_stock_item": 1})
if not frappe.db.exists("UOM", "lbs"):
frappe.get_doc({"doctype": "UOM", "uom_name": "lbs", "must_be_whole_number": 1}).insert()
else:
frappe.db.set_value("UOM", "lbs", "must_be_whole_number", 1)
quotation = make_quotation(item_code=item, qty=1, rate=100, do_not_submit=1)
quotation.items[0].uom = "lbs"
quotation.items[0].conversion_factor = 2.23
self.assertRaises(frappe.ValidationError, quotation.save)
def test_item_tax_template_for_quotation(self):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists("Account", {"account_name": "_Test Vat", "company": "_Test Company"}):
frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Vat",
"company": "_Test Company",
"account_type": "Tax",
"root_type": "Asset",
"is_group": 0,
"parent_account": "Tax Assets - _TC",
"tax_rate": 10,
}
).insert()
if not frappe.db.exists("Item Tax Template", "Vat Template - _TC"):
doc = frappe.get_doc(
{
"doctype": "Item Tax Template",
"name": "Vat Template",
"title": "Vat Template",
"company": "_Test Company",
"taxes": [
{
"tax_type": "_Test Vat - _TC",
"tax_rate": 5,
}
],
}
).insert()
item_doc = make_item("_Test Item Tax Template QTN", {"is_stock_item": 1})
if not frappe.db.exists(
"Item Tax", {"parent": item_doc.name, "item_tax_template": "Vat Template - _TC"}
):
item_doc.append("taxes", {"item_tax_template": "Vat Template - _TC"})
item_doc.save()
quotation = make_quotation(
item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1
)
self.assertFalse(quotation.taxes)
quotation.append_taxes_from_item_tax_template()
quotation.save()
self.assertTrue(quotation.taxes)
for row in quotation.taxes:
self.assertEqual(row.account_head, "_Test Vat - _TC")
self.assertAlmostEqual(row.base_tax_amount, quotation.total * 5 / 100)
item_doc.taxes = []
item_doc.save()
test_records = frappe.get_test_records("Quotation") test_records = frappe.get_test_records("Quotation")

View File

@ -144,15 +144,6 @@ frappe.ui.form.on("Sales Order", {
}; };
}); });
frm.set_query('project', function(doc, cdt, cdn) {
return {
query: "erpnext.controllers.queries.get_project_name",
filters: {
'customer': doc.customer
}
}
});
frm.set_query('warehouse', 'items', function(doc, cdt, cdn) { frm.set_query('warehouse', 'items', function(doc, cdt, cdn) {
let row = locals[cdt][cdn]; let row = locals[cdt][cdn];
let query = { let query = {

View File

@ -131,6 +131,7 @@
"per_billed", "per_billed",
"per_picked", "per_picked",
"billing_status", "billing_status",
"advance_payment_status",
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break7", "column_break7",
@ -1269,7 +1270,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed", "options": "\nDraft\nOn Hold\nTo Pay\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
@ -1638,6 +1639,18 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"report_hide": 1 "report_hide": 1
},
{
"fieldname": "advance_payment_status",
"fieldtype": "Select",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"in_standard_filter": 1,
"label": "Advance Payment Status",
"no_copy": 1,
"options": "Not Requested\nRequested\nPartially Paid\nFully Paid",
"print_hide": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",

View File

@ -223,6 +223,8 @@ class SalesOrder(SellingController):
self.billing_status = "Not Billed" self.billing_status = "Not Billed"
if not self.delivery_status: if not self.delivery_status:
self.delivery_status = "Not Delivered" self.delivery_status = "Not Delivered"
if not self.advance_payment_status:
self.advance_payment_status = "Not Requested"
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
@ -641,7 +643,7 @@ class SalesOrder(SellingController):
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
frappe.throw( frappe.throw(
_( _(
"Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No" "Item {0} has no Serial No. Only serialized items can have delivery based on Serial No"
).format(item.item_code) ).format(item.item_code)
) )
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):

View File

@ -1,6 +1,6 @@
frappe.listview_settings['Sales Order'] = { frappe.listview_settings['Sales Order'] = {
add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date", add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date",
"per_delivered", "per_billed", "status", "order_type", "name", "skip_delivery_note"], "per_delivered", "per_billed", "status", "advance_payment_status", "order_type", "name", "skip_delivery_note"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status === "Closed") { if (doc.status === "Closed") {
// Closed // Closed
@ -10,6 +10,8 @@ frappe.listview_settings['Sales Order'] = {
return [__("On Hold"), "orange", "status,=,On Hold"]; return [__("On Hold"), "orange", "status,=,On Hold"];
} else if (doc.status === "Completed") { } else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"]; return [__("Completed"), "green", "status,=,Completed"];
} else if (doc.advance_payment_status === "Requested") {
return [__("To Pay"), "gray", "advance_payment_status,=,Requested"];
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) { } else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) {
if (frappe.datetime.get_diff(doc.delivery_date) < 0) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
// not delivered & overdue // not delivered & overdue

View File

@ -1996,6 +1996,33 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
def test_sales_order_advance_payment_status(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
so = make_sales_order(qty=1, rate=100)
self.assertEqual(
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
)
pr = make_payment_request(dt=so.doctype, dn=so.name, submit_doc=True, return_doc=True)
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
pe = get_payment_entry(so.doctype, so.name).save().submit()
self.assertEqual(
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid"
)
pe.reload()
pe.cancel()
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
pr.reload()
pr.cancel()
self.assertEqual(
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
)
def automatically_fetch_payment_terms(enable=1): def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings = frappe.get_doc("Accounts Settings")

View File

@ -360,7 +360,7 @@ erpnext.PointOfSale.Controller = class {
this.order_summary.load_summary_of(this.frm.doc, true); this.order_summary.load_summary_of(this.frm.doc, true);
frappe.show_alert({ frappe.show_alert({
indicator: 'green', indicator: 'green',
message: __('POS invoice {0} created succesfully', [r.doc.name]) message: __('POS invoice {0} created successfully', [r.doc.name])
}); });
}); });
} }

View File

@ -209,8 +209,7 @@ def get_so_with_invoices(filters):
) )
.where( .where(
(so.docstatus == 1) (so.docstatus == 1)
& (so.status.isin(["To Deliver and Bill", "To Bill"])) & (so.status.isin(["To Deliver and Bill", "To Bill", "To Pay"]))
& (so.payment_terms_template != "NULL")
& (so.company == conditions.company) & (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date]) & (so.transaction_date[conditions.start_date : conditions.end_date])
) )

View File

@ -56,7 +56,7 @@ frappe.query_reports["Sales Order Analysis"] = {
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
"width": "80", "width": "80",
get_data: function(txt) { get_data: function(txt) {
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"] let status = ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"]
let options = [] let options = []
for (let option of status){ for (let option of status){
options.push({ options.push({

View File

@ -54,7 +54,7 @@ class AuthorizationControl(TransactionBase):
if not has_common(appr_roles, frappe.get_roles()) and not has_common( if not has_common(appr_roles, frappe.get_roles()) and not has_common(
appr_users, [session["user"]] appr_users, [session["user"]]
): ):
frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on))) frappe.msgprint(_("Not authorized since {0} exceeds limits").format(_(based_on)))
frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users))) frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users)))
def validate_auth_rule(self, doctype_name, total, based_on, cond, company, master_name=""): def validate_auth_rule(self, doctype_name, total, based_on, cond, company, master_name=""):

View File

@ -140,6 +140,14 @@ frappe.ui.form.on("Company", {
}, },
delete_company_transactions: function(frm) { delete_company_transactions: function(frm) {
frappe.call({
method: "erpnext.setup.doctype.company.company.is_deletion_job_running",
args: {
company: frm.doc.name
},
freeze: true,
callback: function(r) {
if(!r.exc) {
frappe.verify_password(function() { frappe.verify_password(function() {
var d = frappe.prompt({ var d = frappe.prompt({
fieldtype:"Data", fieldtype:"Data",
@ -159,10 +167,7 @@ frappe.ui.form.on("Company", {
company: data.company_name company: data.company_name
}, },
freeze: true, freeze: true,
callback: function(r, rt) { callback: function(r, rt) { },
if(!r.exc)
frappe.msgprint(__("Successfully deleted all transactions related to this company!"));
},
onerror: function() { onerror: function() {
frappe.msgprint(__("Wrong Password")); frappe.msgprint(__("Wrong Password"));
} }
@ -173,6 +178,11 @@ frappe.ui.form.on("Company", {
d.get_primary_btn().addClass("btn-danger"); d.get_primary_btn().addClass("btn-danger");
}); });
} }
},
});
}
}); });

View File

@ -11,7 +11,8 @@ from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import cint, formatdate, get_timestamp, today from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today
from frappe.utils.background_jobs import get_job, is_job_enqueued
from frappe.utils.nestedset import NestedSet, rebuild_tree from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.accounts.doctype.account.account import get_account_currency
@ -900,8 +901,37 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
return None return None
def generate_id_for_deletion_job(company):
return "delete_company_transactions_" + company
@frappe.whitelist()
def is_deletion_job_running(company):
job_id = generate_id_for_deletion_job(company)
if is_job_enqueued(job_id):
job_name = get_job(job_id).get_id() # job name will have site prefix
frappe.throw(
_("A Transaction Deletion Job: {0} is already running for {1}").format(
frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company)
)
)
@frappe.whitelist() @frappe.whitelist()
def create_transaction_deletion_request(company): def create_transaction_deletion_request(company):
is_deletion_job_running(company)
job_id = generate_id_for_deletion_job(company)
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
tdr.insert() tdr.insert()
tdr.submit()
frappe.enqueue(
"frappe.utils.background_jobs.run_doc_method",
doctype=tdr.doctype,
name=tdr.name,
doc_method="submit",
job_id=job_id,
queue="long",
enqueue_after_commit=True,
)
frappe.msgprint(_("A Transaction Deletion Job is triggered for {0}").format(frappe.bold(company)))

View File

@ -441,13 +441,13 @@
{ {
"fieldname": "prefered_contact_email", "fieldname": "prefered_contact_email",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Prefered Contact Email", "label": "Preferred Contact Email",
"options": "\nCompany Email\nPersonal Email\nUser ID" "options": "\nCompany Email\nPersonal Email\nUser ID"
}, },
{ {
"fieldname": "prefered_email", "fieldname": "prefered_email",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Prefered Email", "label": "Preferred Email",
"options": "Email", "options": "Email",
"read_only": 1 "read_only": 1
}, },
@ -524,7 +524,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"fieldname": "place_of_issue", "fieldname": "place_of_issue",
@ -824,7 +824,7 @@
"image_field": "image", "image_field": "image",
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2024-01-03 17:36:20.984421", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Employee", "name": "Employee",

View File

@ -4,9 +4,10 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
class TestTransactionDeletionRecord(unittest.TestCase): class TestTransactionDeletionRecord(FrappeTestCase):
def setUp(self): def setUp(self):
create_company("Dunder Mifflin Paper Co") create_company("Dunder Mifflin Paper Co")
@ -14,7 +15,7 @@ class TestTransactionDeletionRecord(unittest.TestCase):
frappe.db.rollback() frappe.db.rollback()
def test_doctypes_contain_company_field(self): def test_doctypes_contain_company_field(self):
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
for doctype in tdr.doctypes: for doctype in tdr.doctypes:
contains_company = False contains_company = False
doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"] doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"]
@ -27,17 +28,27 @@ class TestTransactionDeletionRecord(unittest.TestCase):
def test_no_of_docs_is_correct(self): def test_no_of_docs_is_correct(self):
for i in range(5): for i in range(5):
create_task("Dunder Mifflin Paper Co") create_task("Dunder Mifflin Paper Co")
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
for doctype in tdr.doctypes: for doctype in tdr.doctypes:
if doctype.doctype_name == "Task": if doctype.doctype_name == "Task":
self.assertEqual(doctype.no_of_docs, 5) self.assertEqual(doctype.no_of_docs, 5)
def test_deletion_is_successful(self): def test_deletion_is_successful(self):
create_task("Dunder Mifflin Paper Co") create_task("Dunder Mifflin Paper Co")
create_transaction_deletion_request("Dunder Mifflin Paper Co") create_transaction_deletion_doc("Dunder Mifflin Paper Co")
tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"}) tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"})
self.assertEqual(tasks_containing_company, []) self.assertEqual(tasks_containing_company, [])
def test_company_transaction_deletion_request(self):
from erpnext.setup.doctype.company.company import create_transaction_deletion_request
# don't reuse below company for other test cases
company = "Deep Space Exploration"
create_company(company)
# below call should not raise any exceptions or throw errors
create_transaction_deletion_request(company)
def create_company(company_name): def create_company(company_name):
company = frappe.get_doc( company = frappe.get_doc(
@ -46,7 +57,7 @@ def create_company(company_name):
company.insert(ignore_if_duplicate=True) company.insert(ignore_if_duplicate=True)
def create_transaction_deletion_request(company): def create_transaction_deletion_doc(company):
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
tdr.insert() tdr.insert()
tdr.submit() tdr.submit()

View File

@ -52,7 +52,7 @@ frappe.ui.form.on('Batch', {
// sort by qty // sort by qty
r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 }); r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 });
var rows = $('<div></div>').appendTo(section); const rows = $('<div></div>').appendTo(section);
// show // show
(r.message || []).forEach(function(d) { (r.message || []).forEach(function(d) {
@ -76,7 +76,7 @@ frappe.ui.form.on('Batch', {
// move - ask for target warehouse and make stock entry // move - ask for target warehouse and make stock entry
rows.find('.btn-move').on('click', function() { rows.find('.btn-move').on('click', function() {
var $btn = $(this); const $btn = $(this);
const fields = [ const fields = [
{ {
fieldname: 'to_warehouse', fieldname: 'to_warehouse',
@ -115,7 +115,7 @@ frappe.ui.form.on('Batch', {
// split - ask for new qty and batch ID (optional) // split - ask for new qty and batch ID (optional)
// and make stock entry via batch.batch_split // and make stock entry via batch.batch_split
rows.find('.btn-split').on('click', function() { rows.find('.btn-split').on('click', function() {
var $btn = $(this); const $btn = $(this);
frappe.prompt([{ frappe.prompt([{
fieldname: 'qty', fieldname: 'qty',
label: __('New Batch Qty'), label: __('New Batch Qty'),
@ -128,19 +128,16 @@ frappe.ui.form.on('Batch', {
fieldtype: 'Data', fieldtype: 'Data',
}], }],
(data) => { (data) => {
frappe.call({ frappe.xcall(
method: 'erpnext.stock.doctype.batch.batch.split_batch', 'erpnext.stock.doctype.batch.batch.split_batch',
args: { {
item_code: frm.doc.item, item_code: frm.doc.item,
batch_no: frm.doc.name, batch_no: frm.doc.name,
qty: data.qty, qty: data.qty,
warehouse: $btn.attr('data-warehouse'), warehouse: $btn.attr('data-warehouse'),
new_batch_id: data.new_batch_id new_batch_id: data.new_batch_id
}, }
callback: (r) => { ).then(() => frm.reload_doc());
frm.refresh();
},
});
}, },
__('Split Batch'), __('Split Batch'),
__('Split') __('Split')

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CurDate, Sum from frappe.query_builder.functions import CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime, today from frappe.utils import cint, flt, get_link_to_form
from frappe.utils.data import add_days from frappe.utils.data import add_days
from frappe.utils.jinja import render_template from frappe.utils.jinja import render_template
@ -248,8 +248,9 @@ def get_batches_by_oldest(item_code, warehouse):
@frappe.whitelist() @frappe.whitelist()
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): def split_batch(
batch_no: str, item_code: str, warehouse: str, qty: float, new_batch_id: str | None = None
):
"""Split the batch into a new batch""" """Split the batch into a new batch"""
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert() batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
qty = flt(qty) qty = flt(qty)
@ -257,29 +258,21 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
company = frappe.db.get_value("Warehouse", warehouse, "company") company = frappe.db.get_value("Warehouse", warehouse, "company")
from_bundle_id = make_batch_bundle( from_bundle_id = make_batch_bundle(
frappe._dict( item_code=item_code,
{ warehouse=warehouse,
"item_code": item_code, batches=frappe._dict({batch_no: qty}),
"warehouse": warehouse, company=company,
"batches": frappe._dict({batch_no: qty}), type_of_transaction="Outward",
"company": company, qty=qty,
"type_of_transaction": "Outward",
"qty": qty,
}
)
) )
to_bundle_id = make_batch_bundle( to_bundle_id = make_batch_bundle(
frappe._dict( item_code=item_code,
{ warehouse=warehouse,
"item_code": item_code, batches=frappe._dict({batch.name: qty}),
"warehouse": warehouse, company=company,
"batches": frappe._dict({batch.name: qty}), type_of_transaction="Inward",
"company": company, qty=qty,
"type_of_transaction": "Inward",
"qty": qty,
}
)
) )
stock_entry = frappe.get_doc( stock_entry = frappe.get_doc(
@ -304,21 +297,30 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
return batch.name return batch.name
def make_batch_bundle(kwargs): def make_batch_bundle(
item_code: str,
warehouse: str,
batches: dict[str, float],
company: str,
type_of_transaction: str,
qty: float,
):
from frappe.utils import nowtime, today
from erpnext.stock.serial_batch_bundle import SerialBatchCreation from erpnext.stock.serial_batch_bundle import SerialBatchCreation
return ( return (
SerialBatchCreation( SerialBatchCreation(
{ {
"item_code": kwargs.item_code, "item_code": item_code,
"warehouse": kwargs.warehouse, "warehouse": warehouse,
"posting_date": today(), "posting_date": today(),
"posting_time": nowtime(), "posting_time": nowtime(),
"voucher_type": "Stock Entry", "voucher_type": "Stock Entry",
"qty": flt(kwargs.qty), "qty": qty,
"type_of_transaction": kwargs.type_of_transaction, "type_of_transaction": type_of_transaction,
"company": kwargs.company, "company": company,
"batches": kwargs.batches, "batches": batches,
"do_not_submit": True, "do_not_submit": True,
} }
) )

View File

@ -31,15 +31,6 @@ frappe.ui.form.on("Delivery Note", {
}); });
erpnext.queries.setup_warehouse_query(frm); erpnext.queries.setup_warehouse_query(frm);
frm.set_query('project', function(doc) {
return {
query: "erpnext.controllers.queries.get_project_name",
filters: {
'customer': doc.customer
}
}
})
frm.set_query('transporter', function() { frm.set_query('transporter', function() {
return { return {
filters: { filters: {

View File

@ -796,36 +796,36 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
updated_dn = [] updated_dn = []
for dnd in dn_details: for dnd in dn_details:
billed_amt_agianst_dn = 0 billed_amt_against_dn = 0
# If delivered against Sales Invoice # If delivered against Sales Invoice
if dnd.si_detail: if dnd.si_detail:
billed_amt_agianst_dn = flt(dnd.amount) billed_amt_against_dn = flt(dnd.amount)
billed_against_so -= billed_amt_agianst_dn billed_against_so -= billed_amt_against_dn
else: else:
# Get billed amount directly against Delivery Note # Get billed amount directly against Delivery Note
billed_amt_agianst_dn = frappe.db.sql( billed_amt_against_dn = frappe.db.sql(
"""select sum(amount) from `tabSales Invoice Item` """select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", where dn_detail=%s and docstatus=1""",
dnd.name, dnd.name,
) )
billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0 billed_amt_against_dn = billed_amt_against_dn and billed_amt_against_dn[0][0] or 0
# Distribute billed amount directly against SO between DNs based on FIFO # Distribute billed amount directly against SO between DNs based on FIFO
if billed_against_so and billed_amt_agianst_dn < dnd.amount: if billed_against_so and billed_amt_against_dn < dnd.amount:
pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn pending_to_bill = flt(dnd.amount) - billed_amt_against_dn
if pending_to_bill <= billed_against_so: if pending_to_bill <= billed_against_so:
billed_amt_agianst_dn += pending_to_bill billed_amt_against_dn += pending_to_bill
billed_against_so -= pending_to_bill billed_against_so -= pending_to_bill
else: else:
billed_amt_agianst_dn += billed_against_so billed_amt_against_dn += billed_against_so
billed_against_so = 0 billed_against_so = 0
frappe.db.set_value( frappe.db.set_value(
"Delivery Note Item", "Delivery Note Item",
dnd.name, dnd.name,
"billed_amt", "billed_amt",
billed_amt_agianst_dn, billed_amt_against_dn,
update_modified=update_modified, update_modified=update_modified,
) )

View File

@ -191,7 +191,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"fieldname": "section_break_24", "fieldname": "section_break_24",
@ -220,7 +220,7 @@
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-11-15 08:26:04.041861", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Price", "name": "Item Price",

View File

@ -59,7 +59,7 @@ class ItemPrice(Document):
def validate_dates(self): def validate_dates(self):
if self.valid_from and self.valid_upto: if self.valid_from and self.valid_upto:
if getdate(self.valid_from) > getdate(self.valid_upto): if getdate(self.valid_from) > getdate(self.valid_upto):
frappe.throw(_("Valid From Date must be lesser than Valid Upto Date.")) frappe.throw(_("Valid From Date must be lesser than Valid Up To Date."))
def update_price_list_details(self): def update_price_list_details(self):
if self.price_list: if self.price_list:

View File

@ -64,7 +64,7 @@ class TestItemPrice(FrappeTestCase):
# Enter invalid dates valid_from >= valid_upto # Enter invalid dates valid_from >= valid_upto
doc.valid_from = "2017-04-20" doc.valid_from = "2017-04-20"
doc.valid_upto = "2017-04-17" doc.valid_upto = "2017-04-17"
# Valid Upto Date can not be less/equal than Valid From Date # Valid Up To Date can not be less/equal than Valid From Date
self.assertRaises(frappe.ValidationError, doc.save) self.assertRaises(frappe.ValidationError, doc.save)
def test_price_in_a_qty(self): def test_price_in_a_qty(self):

View File

@ -776,7 +776,7 @@ def raise_work_orders(material_request):
) )
else: else:
msgprint( msgprint(
_("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0]) _("The {0} {1} created successfully").format(frappe.bold(_("Work Order")), work_orders_list[0])
) )
if errors: if errors:

View File

@ -24,7 +24,7 @@ frappe.listview_settings['Material Request'] = {
} else if (doc.material_request_type == "Purchase") { } else if (doc.material_request_type == "Purchase") {
return [__("Ordered"), "green", "per_ordered,=,100"]; return [__("Ordered"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Material Transfer") { } else if (doc.material_request_type == "Material Transfer") {
return [__("Transfered"), "green", "per_ordered,=,100"]; return [__("Transferred"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Material Issue") { } else if (doc.material_request_type == "Material Issue") {
return [__("Issued"), "green", "per_ordered,=,100"]; return [__("Issued"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Customer Provided") { } else if (doc.material_request_type == "Customer Provided") {

View File

@ -774,6 +774,62 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(mr.per_ordered, 100) self.assertEqual(mr.per_ordered, 100)
self.assertEqual(existing_requested_qty, current_requested_qty) self.assertEqual(existing_requested_qty, current_requested_qty)
def test_auto_email_users_with_company_user_permissions(self):
from erpnext.stock.reorder_item import get_email_list
comapnywise_users = {
"_Test Company": "test_auto_email_@example.com",
"_Test Company 1": "test_auto_email_1@example.com",
}
permissions = []
for company, user in comapnywise_users.items():
if not frappe.db.exists("User", user):
frappe.get_doc(
{
"doctype": "User",
"email": user,
"first_name": user,
"send_notifications": 0,
"enabled": 1,
"user_type": "System User",
"roles": [{"role": "Purchase Manager"}],
}
).insert(ignore_permissions=True)
if not frappe.db.exists(
"User Permission", {"user": user, "allow": "Company", "for_value": company}
):
perm_doc = frappe.get_doc(
{
"doctype": "User Permission",
"user": user,
"allow": "Company",
"for_value": company,
"apply_to_all_doctypes": 1,
}
).insert(ignore_permissions=True)
permissions.append(perm_doc)
comapnywise_mr_list = frappe._dict({})
mr1 = make_material_request()
comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name)
mr2 = make_material_request(
company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1"
)
comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name)
for company, mr_list in comapnywise_mr_list.items():
emails = get_email_list(company)
self.assertTrue(comapnywise_users[company] in emails)
for perm in permissions:
perm.delete()
def get_in_transit_warehouse(company): def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"): if not frappe.db.exists("Warehouse Type", "Transit"):

View File

@ -951,32 +951,32 @@ def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=No
billed_against_po = flt(po_billed_amt_details.get(pr_item.purchase_order_item)) billed_against_po = flt(po_billed_amt_details.get(pr_item.purchase_order_item))
# Get billed amount directly against Purchase Receipt # Get billed amount directly against Purchase Receipt
billed_amt_agianst_pr = flt(pr_items_billed_amount.get(pr_item.name, 0)) billed_amt_against_pr = flt(pr_items_billed_amount.get(pr_item.name, 0))
# Distribute billed amount directly against PO between PRs based on FIFO # Distribute billed amount directly against PO between PRs based on FIFO
if billed_against_po and billed_amt_agianst_pr < pr_item.amount: if billed_against_po and billed_amt_against_pr < pr_item.amount:
pending_to_bill = flt(pr_item.amount) - billed_amt_agianst_pr pending_to_bill = flt(pr_item.amount) - billed_amt_against_pr
if pending_to_bill <= billed_against_po: if pending_to_bill <= billed_against_po:
billed_amt_agianst_pr += pending_to_bill billed_amt_against_pr += pending_to_bill
billed_against_po -= pending_to_bill billed_against_po -= pending_to_bill
else: else:
billed_amt_agianst_pr += billed_against_po billed_amt_against_pr += billed_against_po
billed_against_po = 0 billed_against_po = 0
po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po
if pr_item.billed_amt != billed_amt_agianst_pr: if pr_item.billed_amt != billed_amt_against_pr:
# update existing doc if possible # update existing doc if possible
if pr_doc and pr_item.parent == pr_doc.name: if pr_doc and pr_item.parent == pr_doc.name:
pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None) pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None)
pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified) pr_item.db_set("billed_amt", billed_amt_against_pr, update_modified=update_modified)
else: else:
frappe.db.set_value( frappe.db.set_value(
"Purchase Receipt Item", "Purchase Receipt Item",
pr_item.name, pr_item.name,
"billed_amt", "billed_amt",
billed_amt_agianst_pr, billed_amt_against_pr,
update_modified=update_modified, update_modified=update_modified,
) )

View File

@ -74,7 +74,7 @@ frappe.ui.form.on('Serial and Batch Bundle', {
let fields = [ let fields = [
{ {
"label": __("Using CSV File"), "label": __("Import Using CSV file"),
"fieldname": "using_csv_file", "fieldname": "using_csv_file",
"default": 1, "default": 1,
"fieldtype": "Check", "fieldtype": "Check",

View File

@ -250,6 +250,7 @@ class SerialandBatchBundle(Document):
for d in self.entries: for d in self.entries:
available_qty = 0 available_qty = 0
if self.has_serial_no: if self.has_serial_no:
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
else: else:
@ -892,6 +893,13 @@ class SerialandBatchBundle(Document):
elif batch_nos: elif batch_nos:
self.set("entries", batch_nos) self.set("entries", batch_nos)
def delete_serial_batch_entries(self):
SBBE = frappe.qb.DocType("Serial and Batch Entry")
frappe.qb.from_(SBBE).delete().where(SBBE.parent == self.name).run()
self.set("entries", [])
@frappe.whitelist() @frappe.whitelist()
def download_blank_csv_template(content): def download_blank_csv_template(content):
@ -999,9 +1007,25 @@ def get_serial_batch_from_data(item_code, kwargs):
make_serial_nos(item_code, serial_nos) make_serial_nos(item_code, serial_nos)
if kwargs.get("_has_serial_nos"):
return serial_nos
return serial_nos, batch_nos return serial_nos, batch_nos
@frappe.whitelist()
def create_serial_nos(item_code, serial_nos):
serial_nos = get_serial_batch_from_data(
item_code,
{
"serial_nos": serial_nos,
"_has_serial_nos": True,
},
)
return serial_nos
def make_serial_nos(item_code, serial_nos): def make_serial_nos(item_code, serial_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
@ -1358,8 +1382,10 @@ def get_available_serial_nos(kwargs):
elif kwargs.based_on == "Expiry": elif kwargs.based_on == "Expiry":
order_by = "amc_expiry_date asc" order_by = "amc_expiry_date asc"
filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")} filters = {"item_code": kwargs.item_code}
if not kwargs.get("ignore_warehouse"):
filters["warehouse"] = ("is", "set")
if kwargs.warehouse: if kwargs.warehouse:
filters["warehouse"] = kwargs.warehouse filters["warehouse"] = kwargs.warehouse
@ -2079,6 +2105,35 @@ def get_batch_no_from_serial_no(serial_no):
return frappe.get_cached_value("Serial No", serial_no, "batch_no") return frappe.get_cached_value("Serial No", serial_no, "batch_no")
@frappe.whitelist()
def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, batch_no=None):
if serial_no and not frappe.db.exists("Serial No", serial_no):
if type_of_transaction != "Inward":
frappe.throw(_("Serial No {0} does not exists").format(serial_no))
make_serial_no(serial_no, item_code)
if batch_no and frappe.db.exists("Batch", batch_no):
if type_of_transaction != "Inward":
frappe.throw(_("Batch No {0} does not exists").format(batch_no))
make_batch_no(batch_no, item_code)
def make_serial_no(serial_no, item_code):
serial_no_doc = frappe.new_doc("Serial No")
serial_no_doc.serial_no = serial_no
serial_no_doc.item_code = item_code
serial_no_doc.save(ignore_permissions=True)
def make_batch_no(batch_no, item_code):
batch_doc = frappe.new_doc("Batch")
batch_doc.batch_id = batch_no
batch_doc.item = item_code
batch_doc.save(ignore_permissions=True)
@frappe.whitelist() @frappe.whitelist()
def is_duplicate_serial_no(bundle_id, serial_no): def is_duplicate_serial_no(bundle_id, serial_no):
return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no}) return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})

View File

@ -156,6 +156,7 @@ class StockReconciliation(StockController):
"warehouse": item.warehouse, "warehouse": item.warehouse,
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"ignore_warehouse": 1,
} }
) )
) )
@ -780,7 +781,20 @@ class StockReconciliation(StockController):
current_qty = 0.0 current_qty = 0.0
if row.current_serial_and_batch_bundle: if row.current_serial_and_batch_bundle:
current_qty = self.get_qty_for_serial_and_batch_bundle(row) current_qty = self.get_current_qty_for_serial_or_batch(row)
elif row.serial_no:
item_dict = get_stock_balance_for(
row.item_code,
row.warehouse,
self.posting_date,
self.posting_time,
voucher_no=self.name,
)
current_qty = item_dict.get("qty")
row.current_serial_no = item_dict.get("serial_nos")
row.current_valuation_rate = item_dict.get("rate")
val_rate = item_dict.get("rate")
elif row.batch_no: elif row.batch_no:
current_qty = get_batch_qty_for_stock_reco( current_qty = get_batch_qty_for_stock_reco(
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
@ -788,6 +802,7 @@ class StockReconciliation(StockController):
precesion = row.precision("current_qty") precesion = row.precision("current_qty")
if flt(current_qty, precesion) != flt(row.current_qty, precesion): if flt(current_qty, precesion) != flt(row.current_qty, precesion):
if not row.serial_no:
val_rate = get_valuation_rate( val_rate = get_valuation_rate(
row.item_code, row.item_code,
row.warehouse, row.warehouse,
@ -842,11 +857,56 @@ class StockReconciliation(StockController):
return allow_negative_stock return allow_negative_stock
def get_qty_for_serial_and_batch_bundle(self, row): def get_current_qty_for_serial_or_batch(self, row):
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle) doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
precision = doc.entries[0].precision("qty") current_qty = 0.0
if doc.has_serial_no:
current_qty = self.get_current_qty_for_serial_nos(doc)
elif doc.has_batch_no:
current_qty = self.get_current_qty_for_batch_nos(doc)
current_qty = 0 return abs(current_qty)
def get_current_qty_for_serial_nos(self, doc):
serial_nos_details = get_available_serial_nos(
frappe._dict(
{
"item_code": doc.item_code,
"warehouse": doc.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_no": self.name,
"ignore_warehouse": 1,
}
)
)
if not serial_nos_details:
return 0.0
doc.delete_serial_batch_entries()
current_qty = 0.0
for serial_no_row in serial_nos_details:
current_qty += 1
doc.append(
"entries",
{
"serial_no": serial_no_row.serial_no,
"qty": -1,
"warehouse": doc.warehouse,
"batch_no": serial_no_row.batch_no,
},
)
doc.set_incoming_rate(save=True)
doc.calculate_qty_and_amount(save=True)
doc.db_update_all()
return current_qty
def get_current_qty_for_batch_nos(self, doc):
current_qty = 0.0
precision = doc.entries[0].precision("qty")
for d in doc.entries: for d in doc.entries:
qty = ( qty = (
get_batch_qty( get_batch_qty(
@ -864,7 +924,7 @@ class StockReconciliation(StockController):
current_qty += qty current_qty += qty
return abs(current_qty) return current_qty
def get_batch_qty_for_stock_reco( def get_batch_qty_for_stock_reco(

View File

@ -925,6 +925,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(len(serial_batch_bundle), 0) self.assertEqual(len(serial_batch_bundle), 0)
def test_backdated_purchase_receipt_with_stock_reco(self):
item_code = self.make_item(
properties={
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "TEST-SERIAL-.###",
}
).name
warehouse = "_Test Warehouse - _TC"
# Step - 1: Create a Backdated Purchase Receipt
pr1 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
)
pr1.reload()
serial_nos = sorted(get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle))[:5]
# Step - 2: Create a Stock Reconciliation
sr1 = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=5,
serial_no=serial_nos,
)
data = frappe.get_all(
"Stock Ledger Entry",
fields=["serial_no", "actual_qty", "stock_value_difference"],
filters={"voucher_no": sr1.name, "is_cancelled": 0},
order_by="creation",
)
for d in data:
if d.actual_qty < 0:
self.assertEqual(d.actual_qty, -10.0)
self.assertAlmostEqual(d.stock_value_difference, -1000.0)
else:
self.assertEqual(d.actual_qty, 5.0)
self.assertAlmostEqual(d.stock_value_difference, 500.0)
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
)
data = frappe.get_all(
"Stock Ledger Entry",
fields=["serial_no", "actual_qty", "stock_value_difference"],
filters={"voucher_no": sr1.name, "is_cancelled": 0},
order_by="creation",
)
for d in data:
if d.actual_qty < 0:
self.assertEqual(d.actual_qty, -20.0)
self.assertAlmostEqual(d.stock_value_difference, -3000.0)
else:
self.assertEqual(d.actual_qty, 5.0)
self.assertAlmostEqual(d.stock_value_difference, 500.0)
active_serial_no = frappe.get_all(
"Serial No", filters={"status": "Active", "item_code": item_code}
)
self.assertEqual(len(active_serial_no), 5)
def create_batch_item_with_batch(item_name, batch_id): def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1) batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@ -176,7 +176,7 @@
"description": "No stock transactions can be created or modified before this date.", "description": "No stock transactions can be created or modified before this date.",
"fieldname": "stock_frozen_upto", "fieldname": "stock_frozen_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Stock Frozen Upto" "label": "Stock Frozen Up To"
}, },
{ {
"description": "Stock transactions that are older than the mentioned days cannot be modified.", "description": "Stock transactions that are older than the mentioned days cannot be modified.",
@ -427,7 +427,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-10-18 12:35:30.068799", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@ -145,6 +145,7 @@ def create_material_request(material_requests):
mr.log_error("Unable to create material request") mr.log_error("Unable to create material request")
company_wise_mr = frappe._dict({})
for request_type in material_requests: for request_type in material_requests:
for company in material_requests[request_type]: for company in material_requests[request_type]:
try: try:
@ -206,17 +207,19 @@ def create_material_request(material_requests):
mr.submit() mr.submit()
mr_list.append(mr) mr_list.append(mr)
company_wise_mr.setdefault(company, []).append(mr)
except Exception: except Exception:
_log_exception(mr) _log_exception(mr)
if mr_list: if company_wise_mr:
if getattr(frappe.local, "reorder_email_notify", None) is None: if getattr(frappe.local, "reorder_email_notify", None) is None:
frappe.local.reorder_email_notify = cint( frappe.local.reorder_email_notify = cint(
frappe.db.get_single_value("Stock Settings", "reorder_email_notify") frappe.db.get_single_value("Stock Settings", "reorder_email_notify")
) )
if frappe.local.reorder_email_notify: if frappe.local.reorder_email_notify:
send_email_notification(mr_list) send_email_notification(company_wise_mr)
if exceptions_list: if exceptions_list:
notify_errors(exceptions_list) notify_errors(exceptions_list)
@ -224,20 +227,56 @@ def create_material_request(material_requests):
return mr_list return mr_list
def send_email_notification(mr_list): def send_email_notification(company_wise_mr):
"""Notify user about auto creation of indent""" """Notify user about auto creation of indent"""
email_list = frappe.db.sql_list( for company, mr_list in company_wise_mr.items():
"""select distinct r.parent email_list = get_email_list(company)
from `tabHas Role` r, tabUser p
where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 if not email_list:
and r.role in ('Purchase Manager','Stock Manager') continue
and p.name not in ('Administrator', 'All', 'Guest')"""
)
msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) 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) 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)
)
)
if users:
query = query.where(user_table.name.isin(users))
emails = query.run(as_dict=True)
return list(set([email.email for email in emails]))
def get_comapny_wise_users(company):
users = frappe.get_all(
"User Permission",
filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1},
fields=["user"],
)
return [user.user for user in users]
def notify_errors(exceptions_list): def notify_errors(exceptions_list):
@ -246,7 +285,7 @@ def notify_errors(exceptions_list):
_("Dear System Manager,") _("Dear System Manager,")
+ "<br>" + "<br>"
+ _( + _(
"An error occured for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :" "An error occurred for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
) )
+ "<br>" + "<br>"
) )

View File

@ -22,9 +22,8 @@ def get_columns(filters):
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90}, {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
{ {
"label": _("Voucher Type"), "label": _("Voucher Type"),
"fieldtype": "Link", "fieldtype": "Data",
"fieldname": "voucher_type", "fieldname": "voucher_type",
"options": "DocType",
"width": 160, "width": 160,
}, },
{ {

View File

@ -9,9 +9,18 @@ from typing import Optional, Set, Tuple
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json from frappe.utils import (
cint,
cstr,
flt,
get_link_to_form,
getdate,
now,
nowdate,
nowtime,
parse_json,
)
import erpnext import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@ -439,7 +448,7 @@ def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
reposting_data = get_reposting_data(doc.reposting_data_file) reposting_data = get_reposting_data(doc.reposting_data_file)
if reposting_data and reposting_data.distinct_item_and_warehouse: if reposting_data and reposting_data.distinct_item_and_warehouse:
return reposting_data.distinct_item_and_warehouse return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
distinct_item_warehouses = {} distinct_item_warehouses = {}
@ -457,6 +466,16 @@ def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
return distinct_item_warehouses return distinct_item_warehouses
def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
new_dict = frappe._dict({})
# convert string keys to tuple
for k, v in distinct_items_and_warehouses.items():
new_dict[frappe.safe_eval(k)] = frappe._dict(v)
return new_dict
def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]: def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
if not reposting_data and doc and doc.reposting_data_file: if not reposting_data and doc and doc.reposting_data_file:
reposting_data = get_reposting_data(doc.reposting_data_file) reposting_data = get_reposting_data(doc.reposting_data_file)
@ -702,11 +721,10 @@ class update_entries_after(object):
if ( if (
sle.voucher_type == "Stock Reconciliation" sle.voucher_type == "Stock Reconciliation"
and ( and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
)
and sle.voucher_detail_no and sle.voucher_detail_no
and not self.args.get("sle_id") and not self.args.get("sle_id")
and sle.is_cancelled == 0
): ):
self.reset_actual_qty_for_stock_reco(sle) self.reset_actual_qty_for_stock_reco(sle)
@ -727,6 +745,23 @@ class update_entries_after(object):
if sle.serial_and_batch_bundle: if sle.serial_and_batch_bundle:
self.calculate_valuation_for_serial_batch_bundle(sle) self.calculate_valuation_for_serial_batch_bundle(sle)
elif sle.serial_no and not self.args.get("sle_id"):
# Only run in reposting
self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
self.wh_data.qty_after_transaction = sle.qty_after_transaction
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
self.wh_data.valuation_rate
)
elif (
sle.batch_no
and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
and not self.args.get("sle_id")
):
# Only run in reposting
self.update_batched_values(sle)
else: else:
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
# assert # assert
@ -772,6 +807,45 @@ class update_entries_after(object):
): ):
self.update_outgoing_rate_on_transaction(sle) self.update_outgoing_rate_on_transaction(sle)
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
serial_nos = cstr(sle.serial_no).split("\n")
if incoming_rate < 0:
# wrong incoming rate
incoming_rate = self.wh_data.valuation_rate
stock_value_change = 0
if actual_qty > 0:
stock_value_change = actual_qty * incoming_rate
else:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
if not sle.is_cancelled:
outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
stock_value_change = -1 * outgoing_value
else:
stock_value_change = actual_qty * sle.outgoing_rate
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
if new_stock_qty > 0:
new_stock_value = (
self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
) + stock_value_change
if new_stock_value >= 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(
sle.voucher_type, sle.voucher_detail_no
)
if not allow_zero_rate:
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def reset_actual_qty_for_stock_reco(self, sle): def reset_actual_qty_for_stock_reco(self, sle):
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
@ -785,6 +859,36 @@ class update_entries_after(object):
if abs(sle.actual_qty) == 0.0: if abs(sle.actual_qty) == 0.0:
sle.is_cancelled = 1 sle.is_cancelled = 1
if sle.serial_and_batch_bundle and frappe.get_cached_value(
"Item", sle.item_code, "has_serial_no"
):
self.update_serial_no_status(sle)
def update_serial_no_status(self, sle):
from erpnext.stock.serial_batch_bundle import get_serial_nos
serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
if not serial_nos:
return
warehouse = None
status = "Inactive"
if sle.actual_qty > 0:
warehouse = sle.warehouse
status = "Active"
sn_table = frappe.qb.DocType("Serial No")
query = (
frappe.qb.update(sn_table)
.set(sn_table.warehouse, warehouse)
.set(sn_table.status, status)
.where(sn_table.name.isin(serial_nos))
)
query.run()
def calculate_valuation_for_serial_batch_bundle(self, sle): def calculate_valuation_for_serial_batch_bundle(self, sle):
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
@ -1161,11 +1265,12 @@ class update_entries_after(object):
outgoing_rate = get_batch_incoming_rate( outgoing_rate = get_batch_incoming_rate(
item_code=sle.item_code, item_code=sle.item_code,
warehouse=sle.warehouse, warehouse=sle.warehouse,
serial_and_batch_bundle=sle.serial_and_batch_bundle, batch_no=sle.batch_no,
posting_date=sle.posting_date, posting_date=sle.posting_date,
posting_time=sle.posting_time, posting_time=sle.posting_time,
creation=sle.creation, creation=sle.creation,
) )
if outgoing_rate is None: if outgoing_rate is None:
# This can *only* happen if qty available for the batch is zero. # This can *only* happen if qty available for the batch is zero.
# in such case fall back various other rates. # in such case fall back various other rates.
@ -1439,11 +1544,10 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
def get_batch_incoming_rate( def get_batch_incoming_rate(
item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None item_code, warehouse, batch_no, posting_date, posting_time, creation=None
): ):
sle = frappe.qb.DocType("Stock Ledger Entry") sle = frappe.qb.DocType("Stock Ledger Entry")
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time posting_date, posting_time
@ -1454,28 +1558,13 @@ def get_batch_incoming_rate(
== CombineDatetime(posting_date, posting_time) == CombineDatetime(posting_date, posting_time)
) & (sle.creation < creation) ) & (sle.creation < creation)
batches = frappe.get_all(
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
)
batch_details = ( batch_details = (
frappe.qb.from_(sle) frappe.qb.from_(sle)
.inner_join(batch_ledger) .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
.on(sle.serial_and_batch_bundle == batch_ledger.parent)
.select(
Sum(
Case()
.when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
.else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
).as_("batch_value"),
Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
"batch_qty"
),
)
.where( .where(
(sle.item_code == item_code) (sle.item_code == item_code)
& (sle.warehouse == warehouse) & (sle.warehouse == warehouse)
& (batch_ledger.batch_no.isin([row.batch_no for row in batches])) & (sle.batch_no == batch_no)
& (sle.is_cancelled == 0) & (sle.is_cancelled == 0)
) )
.where(timestamp_condition) .where(timestamp_condition)

View File

@ -124,7 +124,7 @@ def get_help_messages():
doctype="Timesheet", doctype="Timesheet",
title=_("Add Timesheets"), title=_("Add Timesheets"),
description=_( description=_(
"Timesheets help keep track of time, cost and billing for activites done by your team" "Timesheets help keep track of time, cost and billing for activities done by your team"
), ),
action=_("Create Timesheet"), action=_("Create Timesheet"),
route="List/Timesheet", route="List/Timesheet",

View File

@ -8,26 +8,29 @@
"allow_print": 0, "allow_print": 0,
"amount": 0.0, "amount": 0.0,
"amount_based_on_field": 0, "amount_based_on_field": 0,
"anonymous": 0,
"apply_document_permissions": 1,
"condition_json": "[]",
"creation": "2016-06-24 15:50:33.196990", "creation": "2016-06-24 15:50:33.196990",
"doc_type": "Address", "doc_type": "Address",
"docstatus": 0, "docstatus": 0,
"doctype": "Web Form", "doctype": "Web Form",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"list_columns": [],
"list_title": "",
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2019-10-15 06:55:30.405119", "modified": "2024-01-24 10:28:35.026064",
"modified_by": "Administrator", "modified_by": "rohitw1991@gmail.com",
"module": "Utilities", "module": "Utilities",
"name": "addresses", "name": "addresses",
"owner": "Administrator", "owner": "Administrator",
"published": 1, "published": 1,
"route": "address", "route": "address",
"route_to_success_link": 0,
"show_attachments": 0, "show_attachments": 0,
"show_in_grid": 0, "show_list": 1,
"show_sidebar": 0, "show_sidebar": 0,
"sidebar_items": [],
"success_url": "/addresses", "success_url": "/addresses",
"title": "Address", "title": "Address",
"web_form_fields": [ "web_form_fields": [