Merge branch 'develop' into manufacturing-work-order-stop
This commit is contained in:
commit
8ebc412e8b
23
.github/workflows/patch.yml
vendored
23
.github/workflows/patch.yml
vendored
@ -86,4 +86,27 @@ jobs:
|
||||
cd ~/frappe-bench/
|
||||
wget https://erpnext.com/files/v10-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
|
||||
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version"
|
||||
|
||||
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
|
||||
git -C "apps/frappe" checkout -q -f $branch_name
|
||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||
|
||||
bench setup requirements --python
|
||||
bench --site test_site migrate
|
||||
done
|
||||
|
||||
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
bench --site test_site migrate
|
||||
|
@ -81,7 +81,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
def identify_is_group(child):
|
||||
if child.get("is_group"):
|
||||
is_group = child.get("is_group")
|
||||
elif len(set(child.keys()) - set(["account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
|
@ -58,7 +58,8 @@ class GLEntry(Document):
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
|
||||
and self.against_voucher and self.flags.update_outstanding == 'Yes'):
|
||||
and self.against_voucher and self.flags.update_outstanding == 'Yes'
|
||||
and not frappe.flags.is_reverse_depr_entry):
|
||||
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
|
||||
self.against_voucher)
|
||||
|
||||
|
@ -58,7 +58,10 @@ class JournalEntry(AccountsController):
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
self.validate_against_jv()
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
self.validate_reference_doc()
|
||||
if self.docstatus == 0:
|
||||
self.set_against_account()
|
||||
@ -69,7 +72,6 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
@ -389,7 +389,7 @@ class PaymentEntry(AccountsController):
|
||||
invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
|
||||
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
|
||||
|
||||
for key, allocated_amount in iteritems(invoice_payment_amount_map):
|
||||
for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
|
||||
|
||||
@ -407,7 +407,7 @@ class PaymentEntry(AccountsController):
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
|
||||
frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0]))
|
||||
|
||||
if allocated_amount and outstanding:
|
||||
frappe.db.sql("""
|
||||
@ -1053,12 +1053,6 @@ def get_outstanding_reference_documents(args):
|
||||
party_account_currency = get_account_currency(args.get("party_account"))
|
||||
company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
|
||||
args.get("party_account"), args.get("company"), party_account_currency, company_currency)
|
||||
|
||||
# Get positive outstanding sales /purchase invoices/ Fees
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
@ -1105,6 +1099,12 @@ def get_outstanding_reference_documents(args):
|
||||
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
|
||||
args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args)
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
|
||||
args.get("party_account"), party_account_currency, company_currency, condition=condition)
|
||||
|
||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||
|
||||
if not data:
|
||||
@ -1137,22 +1137,26 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
|
||||
'invoice_amount': flt(d.invoice_amount),
|
||||
'outstanding_amount': flt(d.outstanding_amount),
|
||||
'payment_amount': payment_term.payment_amount,
|
||||
'payment_term': payment_term.payment_term,
|
||||
'allocated_amount': payment_term.outstanding
|
||||
'payment_term': payment_term.payment_term
|
||||
}))
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = outstanding_invoices[idx]['voucher_no']
|
||||
voucher_type = outstanding_invoices[idx]['voucher_type']
|
||||
voucher_no = ref[0]['voucher_no']
|
||||
voucher_type = ref[0]['voucher_type']
|
||||
|
||||
frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
|
||||
frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
voucher_type, voucher_no, len(ref)), alert=True)
|
||||
|
||||
outstanding_invoices.pop(idx - 1)
|
||||
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
return outstanding_invoices
|
||||
existing_row = list(filter(lambda x: x.get('voucher_no') == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
company, party_account_currency, company_currency, cost_center=None, filters=None):
|
||||
@ -1219,7 +1223,7 @@ def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
return order_list
|
||||
|
||||
def get_negative_outstanding_invoices(party_type, party, party_account,
|
||||
company, party_account_currency, company_currency, cost_center=None):
|
||||
party_account_currency, company_currency, cost_center=None, condition=None):
|
||||
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
supplier_condition = ""
|
||||
if voucher_type == "Purchase Invoice":
|
||||
@ -1241,19 +1245,21 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
|
||||
`tab{voucher_type}`
|
||||
where
|
||||
{party_type} = %s and {party_account} = %s and docstatus = 1 and
|
||||
company = %s and outstanding_amount < 0
|
||||
outstanding_amount < 0
|
||||
{supplier_condition}
|
||||
{condition}
|
||||
order by
|
||||
posting_date, name
|
||||
""".format(**{
|
||||
"supplier_condition": supplier_condition,
|
||||
"condition": condition,
|
||||
"rounded_total_field": rounded_total_field,
|
||||
"grand_total_field": grand_total_field,
|
||||
"voucher_type": voucher_type,
|
||||
"party_type": scrub(party_type),
|
||||
"party_account": "debit_to" if party_type == "Customer" else "credit_to",
|
||||
"cost_center": cost_center
|
||||
}), (party, party_account, company), as_dict=True)
|
||||
}), (party, party_account), as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -4,9 +4,14 @@
|
||||
frappe.provide("erpnext.accounts");
|
||||
erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller {
|
||||
onload() {
|
||||
var me = this;
|
||||
const default_company = frappe.defaults.get_default('company');
|
||||
this.frm.set_value('company', default_company);
|
||||
|
||||
this.frm.set_query("party_type", function() {
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
|
||||
this.frm.set_query("party_type", () => {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
@ -14,44 +19,30 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
});
|
||||
|
||||
this.frm.set_query('receivable_payable_account', function() {
|
||||
check_mandatory(me.frm);
|
||||
this.frm.set_query('receivable_payable_account', () => {
|
||||
return {
|
||||
filters: {
|
||||
"company": me.frm.doc.company,
|
||||
"company": this.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
||||
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query('bank_cash_account', function() {
|
||||
check_mandatory(me.frm, true);
|
||||
this.frm.set_query('bank_cash_account', () => {
|
||||
return {
|
||||
filters:[
|
||||
['Account', 'company', '=', me.frm.doc.company],
|
||||
['Account', 'company', '=', this.frm.doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
|
||||
var check_mandatory = (frm, only_company=false) => {
|
||||
var title = __("Mandatory");
|
||||
if (only_company && !frm.doc.company) {
|
||||
frappe.throw({message: __("Please Select a Company First"), title: title});
|
||||
} else if (!frm.doc.company || !frm.doc.party_type) {
|
||||
frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.frm.disable_save();
|
||||
|
||||
this.frm.set_df_property('invoices', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('payments', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
|
||||
@ -85,76 +76,92 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
|
||||
company() {
|
||||
var me = this;
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
me.frm.clear_table("allocation");
|
||||
me.frm.clear_table("invoices");
|
||||
me.frm.clear_table("payments");
|
||||
me.frm.refresh_fields();
|
||||
me.frm.trigger('party');
|
||||
}
|
||||
|
||||
party_type() {
|
||||
this.frm.set_value('party', '');
|
||||
}
|
||||
|
||||
party() {
|
||||
var me = this;
|
||||
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
this.frm.trigger("clear_child_tables");
|
||||
|
||||
if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
company: me.frm.doc.company,
|
||||
party_type: me.frm.doc.party_type,
|
||||
party: me.frm.doc.party
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party
|
||||
},
|
||||
callback: function(r) {
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
me.frm.set_value("receivable_payable_account", r.message);
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
}
|
||||
me.frm.refresh();
|
||||
this.frm.refresh();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
receivable_payable_account() {
|
||||
this.frm.trigger("clear_child_tables");
|
||||
this.frm.refresh();
|
||||
}
|
||||
|
||||
clear_child_tables() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
get_unreconciled_entries() {
|
||||
var me = this;
|
||||
this.frm.clear_table("allocation");
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
doc: this.frm.doc,
|
||||
method: 'get_unreconciled_entries',
|
||||
callback: function(r, rt) {
|
||||
if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No invoice and payment records found for this party")});
|
||||
callback: () => {
|
||||
if (!(this.frm.doc.payments.length || this.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No Unreconciled Invoices and Payments found for this party and account")});
|
||||
} else if (!(this.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No Outstanding Invoices found for this party")});
|
||||
} else if (!(this.frm.doc.payments.length)) {
|
||||
frappe.throw({message: __("No Unreconciled Payments found for this party")});
|
||||
}
|
||||
me.frm.refresh();
|
||||
this.frm.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
allocate() {
|
||||
var me = this;
|
||||
let payments = me.frm.fields_dict.payments.grid.get_selected_children();
|
||||
let payments = this.frm.fields_dict.payments.grid.get_selected_children();
|
||||
if (!(payments.length)) {
|
||||
payments = me.frm.doc.payments;
|
||||
payments = this.frm.doc.payments;
|
||||
}
|
||||
let invoices = me.frm.fields_dict.invoices.grid.get_selected_children();
|
||||
let invoices = this.frm.fields_dict.invoices.grid.get_selected_children();
|
||||
if (!(invoices.length)) {
|
||||
invoices = me.frm.doc.invoices;
|
||||
invoices = this.frm.doc.invoices;
|
||||
}
|
||||
return me.frm.call({
|
||||
doc: me.frm.doc,
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'allocate_entries',
|
||||
args: {
|
||||
payments: payments,
|
||||
invoices: invoices
|
||||
},
|
||||
callback: function() {
|
||||
me.frm.refresh();
|
||||
callback: () => {
|
||||
this.frm.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reconcile() {
|
||||
var me = this;
|
||||
var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
|
||||
if (show_dialog && show_dialog.length) {
|
||||
|
||||
@ -186,10 +193,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
label: __("Difference Account"),
|
||||
fieldname: 'difference_account',
|
||||
reqd: 1,
|
||||
get_query: function() {
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
company: me.frm.doc.company,
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0
|
||||
}
|
||||
}
|
||||
@ -203,7 +210,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}]
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
primary_action: () => {
|
||||
const args = dialog.get_values()["allocation"];
|
||||
|
||||
args.forEach(d => {
|
||||
@ -211,7 +218,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
"difference_account", d.difference_account);
|
||||
});
|
||||
|
||||
me.reconcile_payment_entries();
|
||||
this.reconcile_payment_entries();
|
||||
dialog.hide();
|
||||
},
|
||||
primary_action_label: __('Reconcile Entries')
|
||||
@ -237,15 +244,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
|
||||
reconcile_payment_entries() {
|
||||
var me = this;
|
||||
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
doc: this.frm.doc,
|
||||
method: 'reconcile',
|
||||
callback: function(r, rt) {
|
||||
me.frm.clear_table("allocation");
|
||||
me.frm.refresh_fields();
|
||||
me.frm.refresh();
|
||||
callback: () => {
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -114,6 +114,8 @@ class POSInvoiceMergeLog(Document):
|
||||
def merge_pos_invoice_into(self, invoice, data):
|
||||
items, payments, taxes = [], [], []
|
||||
loyalty_amount_sum, loyalty_points_sum = 0, 0
|
||||
rounding_adjustment, base_rounding_adjustment = 0, 0
|
||||
rounded_total, base_rounded_total = 0, 0
|
||||
for doc in data:
|
||||
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
|
||||
|
||||
@ -162,6 +164,11 @@ class POSInvoiceMergeLog(Document):
|
||||
found = True
|
||||
if not found:
|
||||
payments.append(payment)
|
||||
rounding_adjustment += doc.rounding_adjustment
|
||||
rounded_total += doc.rounded_total
|
||||
base_rounding_adjustment += doc.rounding_adjustment
|
||||
base_rounded_total += doc.rounded_total
|
||||
|
||||
|
||||
if loyalty_points_sum:
|
||||
invoice.redeem_loyalty_points = 1
|
||||
@ -171,6 +178,10 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.set('items', items)
|
||||
invoice.set('payments', payments)
|
||||
invoice.set('taxes', taxes)
|
||||
invoice.set('rounding_adjustment',rounding_adjustment)
|
||||
invoice.set('rounding_adjustment',base_rounding_adjustment)
|
||||
invoice.set('base_rounded_total',base_rounded_total)
|
||||
invoice.set('rounded_total',rounded_total)
|
||||
invoice.additional_discount_percentage = 0
|
||||
invoice.discount_amount = 0.0
|
||||
invoice.taxes_and_charges = None
|
||||
|
@ -37,7 +37,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
post_depreciation_entries,
|
||||
make_depreciation_entry,
|
||||
)
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||
@ -934,6 +934,7 @@ class SalesInvoice(SellingController):
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
else:
|
||||
@ -997,22 +998,20 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(self.posting_date)
|
||||
asset.prepare_depreciation_data(date_of_sale=self.posting_date)
|
||||
asset.save()
|
||||
|
||||
post_depreciation_entries(self.posting_date)
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data()
|
||||
asset.prepare_depreciation_data(date_of_return=self.posting_date)
|
||||
|
||||
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
|
||||
self.delete_depreciation_entry_made_after_sale(asset)
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
'Asset Repair',
|
||||
@ -1026,7 +1025,7 @@ class SalesInvoice(SellingController):
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
def delete_depreciation_entry_made_after_sale(self, asset):
|
||||
def reverse_depreciation_entry_made_after_sale(self, asset):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
@ -1041,11 +1040,19 @@ class SalesInvoice(SellingController):
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == posting_date_of_original_invoice:
|
||||
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice):
|
||||
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \
|
||||
or self.sale_happens_in_the_future(posting_date_of_original_invoice):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_sales_invoice(self):
|
||||
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
|
||||
|
||||
@ -1060,6 +1067,12 @@ class SalesInvoice(SellingController):
|
||||
return True
|
||||
return False
|
||||
|
||||
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
|
||||
if posting_date_of_original_invoice > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
|
@ -2237,9 +2237,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
enable_discount_accounting(enable=0)
|
||||
|
||||
def test_asset_depreciation_on_sale(self):
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30.
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
"""
|
||||
|
||||
create_asset_data()
|
||||
@ -2252,7 +2252,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48],
|
||||
["2021-06-30", 20000.0, 21311.48],
|
||||
["2021-09-30", 3966.76, 25278.24]
|
||||
["2021-09-30", 5041.1, 26352.58]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@ -2261,6 +2261,59 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
def test_asset_depreciation_on_sale_without_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale.
|
||||
"""
|
||||
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1,
|
||||
available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3,
|
||||
expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1)
|
||||
|
||||
post_depreciation_entries(getdate("2021-09-30"))
|
||||
|
||||
create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31"))
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-12-31", 30000, 30000],
|
||||
["2021-12-31", 30000, 60000]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
def test_depreciation_on_return_of_sold_asset(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
|
||||
post_depreciation_entries(getdate("2021-09-30"))
|
||||
|
||||
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
|
||||
return_si = make_return_doc("Sales Invoice", si.name)
|
||||
return_si.submit()
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48, True],
|
||||
["2021-06-30", 20000.0, 21311.48, True],
|
||||
["2022-06-30", 20000.0, 41311.48, False],
|
||||
["2023-06-30", 20000.0, 61311.48, False],
|
||||
["2024-06-30", 20000.0, 81311.48, False],
|
||||
["2025-06-06", 18688.52, 100000.0, False]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
|
||||
|
||||
def test_sales_invoice_against_supplier(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
|
@ -502,9 +502,11 @@ class Subscription(Document):
|
||||
# Check invoice dates and make sure it doesn't have outstanding invoices
|
||||
return getdate() >= getdate(self.current_invoice_start)
|
||||
|
||||
def is_current_invoice_generated(self):
|
||||
def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
|
||||
invoice = self.get_current_invoice()
|
||||
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
|
||||
|
||||
if not (_current_start_date and _current_end_date):
|
||||
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
|
||||
|
||||
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
|
||||
return True
|
||||
@ -523,7 +525,9 @@ class Subscription(Document):
|
||||
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
|
||||
and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
|
||||
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
@ -559,14 +563,17 @@ class Subscription(Document):
|
||||
else:
|
||||
self.set_status_grace_period()
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
# Generate invoices periodically even if current invoice are unpaid
|
||||
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
|
||||
or self.is_prepaid_to_invoice()):
|
||||
if self.generate_new_invoices_past_due_date and not \
|
||||
self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
|
||||
and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
|
||||
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
@staticmethod
|
||||
def is_paid(invoice):
|
||||
|
@ -58,15 +58,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
pan_no = ''
|
||||
parties = []
|
||||
party_type, party = get_party_details(inv)
|
||||
has_pan_field = frappe.get_meta(party_type).has_field("pan")
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
|
||||
if has_pan_field:
|
||||
fields = ['tax_withholding_category', 'pan']
|
||||
else:
|
||||
fields = ['tax_withholding_category']
|
||||
|
||||
tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1)
|
||||
|
||||
tax_withholding_category = tax_withholding_details.get('tax_withholding_category')
|
||||
pan_no = tax_withholding_details.get('pan')
|
||||
|
||||
if not tax_withholding_category:
|
||||
return
|
||||
|
||||
# if tax_withholding_category passed as an argument but not pan_no
|
||||
if not pan_no:
|
||||
if not pan_no and has_pan_field:
|
||||
pan_no = frappe.db.get_value(party_type, party, 'pan')
|
||||
|
||||
# Get others suppliers with the same PAN No
|
||||
|
@ -450,7 +450,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# new row with references
|
||||
new_row = journal_entry.append("accounts")
|
||||
new_row.update(jv_detail.as_dict().copy())
|
||||
|
||||
new_row.update((frappe.copy_doc(jv_detail)).as_dict())
|
||||
|
||||
new_row.set(d["dr_or_cr"], d["allocated_amount"])
|
||||
new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit',
|
||||
|
@ -75,12 +75,12 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset and self.purchase_invoice:
|
||||
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
|
||||
|
||||
def prepare_depreciation_data(self, date_of_sale=None):
|
||||
def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
self.make_depreciation_schedule(date_of_sale)
|
||||
self.set_accumulated_depreciation(date_of_sale)
|
||||
self.set_accumulated_depreciation(date_of_sale, date_of_return)
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
@ -182,7 +182,7 @@ class Asset(AccountsController):
|
||||
d.precision("rate_of_depreciation"))
|
||||
|
||||
def make_depreciation_schedule(self, date_of_sale):
|
||||
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
|
||||
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'):
|
||||
self.schedules = []
|
||||
|
||||
if not self.available_for_use_date:
|
||||
@ -232,13 +232,15 @@ class Asset(AccountsController):
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
from_date, date_of_sale)
|
||||
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
})
|
||||
if depreciation_amount > 0:
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# For first row
|
||||
@ -257,11 +259,15 @@ class Asset(AccountsController):
|
||||
self.to_date = add_months(self.available_for_use_date,
|
||||
n * cint(d.frequency_of_depreciation))
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d,
|
||||
depreciation_amount, schedule_date, self.to_date)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
||||
depreciation_amount, d.finance_book)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
last_schedule_date = schedule_date
|
||||
|
||||
@ -397,7 +403,28 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
|
||||
.format(row.idx))
|
||||
|
||||
def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False):
|
||||
# to ensure that final accumulated depreciation amount is accurate
|
||||
def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
|
||||
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
|
||||
|
||||
if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
|
||||
depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
|
||||
|
||||
return depreciation_amount_for_last_row
|
||||
|
||||
def get_depreciation_amount_for_first_row(self, finance_book):
|
||||
if self.has_only_one_finance_book():
|
||||
return self.schedules[0].depreciation_amount
|
||||
else:
|
||||
for schedule in self.schedules:
|
||||
if schedule.finance_book == finance_book:
|
||||
return schedule.depreciation_amount
|
||||
|
||||
def has_only_one_finance_book(self):
|
||||
if len(self.finance_books) == 1:
|
||||
return True
|
||||
|
||||
def set_accumulated_depreciation(self, date_of_sale=None, date_of_return=None, ignore_booked_entry = False):
|
||||
straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line']
|
||||
finance_books = []
|
||||
|
||||
@ -414,7 +441,7 @@ class Asset(AccountsController):
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
|
||||
# for the last row, if depreciation method = Straight Line
|
||||
if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale:
|
||||
if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale and not date_of_return:
|
||||
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
|
||||
depreciation_amount += flt(value_after_depreciation -
|
||||
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))
|
||||
@ -833,7 +860,7 @@ def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
if not asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (flt(row.value_after_depreciation) -
|
||||
depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
|
||||
flt(row.expected_value_after_useful_life)) / depreciation_left
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -16,9 +16,8 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
fieldname:"status",
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
options: "In Location\nDisposed",
|
||||
default: 'In Location',
|
||||
reqd: 1
|
||||
options: "\nIn Location\nDisposed",
|
||||
default: 'In Location'
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
|
@ -45,12 +45,13 @@ def get_conditions(filters):
|
||||
if filters.get('cost_center'):
|
||||
conditions["cost_center"] = filters.get('cost_center')
|
||||
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
operand = 'not in'
|
||||
if status not in 'In Location':
|
||||
operand = 'in'
|
||||
if status:
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
operand = 'not in'
|
||||
if status not in 'In Location':
|
||||
operand = 'in'
|
||||
|
||||
conditions['status'] = (operand, ['Sold', 'Scrapped'])
|
||||
conditions['status'] = (operand, ['Sold', 'Scrapped'])
|
||||
|
||||
return conditions
|
||||
|
||||
|
@ -41,10 +41,13 @@ def get_conditions(filters):
|
||||
if filters.get("from_date") and filters.get("to_date"):
|
||||
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
|
||||
|
||||
for field in ['company', 'name', 'status']:
|
||||
for field in ['company', 'name']:
|
||||
if filters.get(field):
|
||||
conditions += f" and po.{field} = %({field})s"
|
||||
|
||||
if filters.get('status'):
|
||||
conditions += " and po.status in %(status)s"
|
||||
|
||||
if filters.get('project'):
|
||||
conditions += " and poi.project = %(project)s"
|
||||
|
||||
|
@ -566,7 +566,7 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
|
||||
|
||||
query_filters.append(['name', query_selector, dimensions])
|
||||
|
||||
output = frappe.get_all(doctype, filters=query_filters)
|
||||
output = frappe.get_list(doctype, filters=query_filters)
|
||||
result = [d.name for d in output]
|
||||
|
||||
return [(d,) for d in set(result)]
|
||||
|
@ -260,7 +260,9 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
|
||||
|
||||
def calculate_taxes(self):
|
||||
self.doc.rounding_adjustment = 0
|
||||
if not self.doc.get('is_consolidated'):
|
||||
self.doc.rounding_adjustment = 0
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
|
||||
for tax in self.doc.get("taxes") if tax.charge_type == "Actual"])
|
||||
@ -312,7 +314,9 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
# adjust Discount Amount loss in last tax iteration
|
||||
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
|
||||
and self.doc.discount_amount and self.doc.apply_discount_on == "Grand Total":
|
||||
and self.doc.discount_amount \
|
||||
and self.doc.apply_discount_on == "Grand Total" \
|
||||
and not self.doc.get('is_consolidated'):
|
||||
self.doc.rounding_adjustment = flt(self.doc.grand_total
|
||||
- flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
@ -405,11 +409,16 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.rounding_adjustment = diff
|
||||
|
||||
def calculate_totals(self):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment) \
|
||||
if self.doc.get("taxes") else flt(self.doc.net_total)
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
|
||||
- flt(self.doc.rounding_adjustment), self.doc.precision("total_taxes_and_charges"))
|
||||
else:
|
||||
self.doc.total_taxes_and_charges = 0.0
|
||||
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
|
||||
|
||||
@ -446,19 +455,20 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.total_net_weight += d.total_weight
|
||||
|
||||
def set_rounded_total(self):
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
return
|
||||
if not self.doc.get('is_consolidated'):
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
return
|
||||
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||
self.doc.currency, self.doc.precision("rounded_total"))
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||
self.doc.currency, self.doc.precision("rounded_total"))
|
||||
|
||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
|
||||
def _cleanup(self):
|
||||
if not self.doc.get('is_consolidated'):
|
||||
|
@ -20,6 +20,7 @@
|
||||
"website",
|
||||
"column_break_13",
|
||||
"prospect_owner",
|
||||
"company",
|
||||
"leads_section",
|
||||
"prospect_lead",
|
||||
"address_and_contact_section",
|
||||
@ -153,14 +154,23 @@
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-27 16:24:42.961967",
|
||||
"migration_hash": "f39fb8f4e18a0e7fd391f0b4b52d8375",
|
||||
"modified": "2021-11-01 13:10:36.759249",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -307,6 +307,9 @@ class BOM(WebsiteGenerator):
|
||||
existing_bom_cost = self.total_cost
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.item_code:
|
||||
continue
|
||||
|
||||
rate = self.get_rm_rate({
|
||||
"company": self.company,
|
||||
"item_code": d.item_code,
|
||||
@ -599,7 +602,7 @@ class BOM(WebsiteGenerator):
|
||||
for d in self.get('items'):
|
||||
if d.bom_no:
|
||||
self.get_child_exploded_items(d.bom_no, d.stock_qty)
|
||||
else:
|
||||
elif d.item_code:
|
||||
self.add_to_cur_exploded_items(frappe._dict({
|
||||
'item_code' : d.item_code,
|
||||
'item_name' : d.item_name,
|
||||
|
@ -6,6 +6,8 @@ def execute():
|
||||
if not company:
|
||||
return
|
||||
|
||||
frappe.reload_doc('regional', 'doctype', 'lower_deduction_certificate')
|
||||
|
||||
ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc")
|
||||
supplier = frappe.qb.DocType("Supplier")
|
||||
|
||||
|
@ -31,7 +31,7 @@ class LowerDeductionCertificate(Document):
|
||||
<= fiscal_year.year_end_date):
|
||||
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
|
||||
|
||||
def tax_withholding_category(self):
|
||||
def validate_supplier_against_tax_category(self):
|
||||
duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate',
|
||||
{'supplier': self.supplier, 'tax_withholding_category': self.tax_withholding_category, 'name': ("!=", self.name)},
|
||||
['name', 'valid_from', 'valid_upto'], as_dict=True)
|
||||
|
@ -855,7 +855,7 @@ def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
if not asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (flt(row.value_after_depreciation) -
|
||||
depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
|
||||
flt(row.expected_value_after_useful_life)) / depreciation_left
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair
|
||||
|
@ -122,7 +122,7 @@ def get_total_emiratewise(filters):
|
||||
try:
|
||||
return frappe.db.sql("""
|
||||
select
|
||||
s.vat_emirate as emirate, sum(i.base_amount) as total, sum(s.total_taxes_and_charges)
|
||||
s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount)
|
||||
from
|
||||
`tabSales Invoice Item` i inner join `tabSales Invoice` s
|
||||
on
|
||||
|
@ -206,8 +206,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
var me = this;
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
|
||||
if (item.serial_no && item.qty === item.serial_no.split(`\n`).length) {
|
||||
return;
|
||||
// check if serial nos entered are as much as qty in row
|
||||
if (item.serial_no) {
|
||||
let serial_nos = item.serial_no.split(`\n`).filter(sn => sn.trim()); // filter out whitespaces
|
||||
if (item.qty === serial_nos.length) return;
|
||||
}
|
||||
|
||||
if (item.serial_no && !item.batch_no) {
|
||||
|
@ -1195,7 +1195,7 @@
|
||||
"*": {
|
||||
"item_tax_templates": [
|
||||
{
|
||||
"title": "GST 9%",
|
||||
"title": "GST 18%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
|
@ -676,6 +676,8 @@ class Item(WebsiteGenerator):
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
if merge:
|
||||
self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name)
|
||||
frappe.msgprint(_("It can take upto few hours for accurate stock values to be visible after merging items."),
|
||||
indicator="orange", title="Note")
|
||||
|
||||
if self.route:
|
||||
invalidate_cache_for_item(self)
|
||||
|
@ -88,7 +88,11 @@ frappe.ui.form.on('Stock Entry', {
|
||||
}
|
||||
}
|
||||
|
||||
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
|
||||
// User could want to select a manually created empty batch (no warehouse)
|
||||
// or a pre-existing batch
|
||||
if (frm.doc.purpose != "Material Receipt") {
|
||||
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
|
||||
}
|
||||
|
||||
return {
|
||||
query : "erpnext.controllers.queries.get_batch_no",
|
||||
|
@ -600,7 +600,7 @@ class update_entries_after(object):
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company))
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
|
||||
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
|
||||
# get rate from serial nos within same company
|
||||
@ -667,7 +667,7 @@ class update_entries_after(object):
|
||||
if not allow_zero_valuation_rate:
|
||||
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company))
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
|
||||
def get_fifo_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
@ -700,7 +700,7 @@ class update_entries_after(object):
|
||||
if not allow_zero_valuation_rate:
|
||||
_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company))
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
else:
|
||||
_rate = 0
|
||||
|
||||
@ -911,10 +911,11 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
|
||||
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
|
||||
allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
|
||||
# Get valuation rate from last sle for the same item and warehouse
|
||||
if not company:
|
||||
company = erpnext.get_default_company()
|
||||
|
||||
if not company:
|
||||
company = frappe.get_cached_value("Warehouse", warehouse, "company")
|
||||
|
||||
# Get valuation rate from last sle for the same item and warehouse
|
||||
last_valuation_rate = frappe.db.sql("""select valuation_rate
|
||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||
where
|
||||
|
@ -101,11 +101,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
|
||||
|
||||
if with_valuation_rate:
|
||||
if with_serial_no:
|
||||
serial_nos = last_entry.get("serial_no")
|
||||
|
||||
if (serial_nos and
|
||||
len(get_serial_nos_data(serial_nos)) < last_entry.qty_after_transaction):
|
||||
serial_nos = get_serial_nos_data_after_transactions(args)
|
||||
serial_nos = get_serial_nos_data_after_transactions(args)
|
||||
|
||||
return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
|
||||
if last_entry else (0.0, 0.0, 0.0))
|
||||
@ -115,19 +111,32 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
|
||||
return last_entry.qty_after_transaction if last_entry else 0.0
|
||||
|
||||
def get_serial_nos_data_after_transactions(args):
|
||||
serial_nos = []
|
||||
data = frappe.db.sql(""" SELECT serial_no, actual_qty
|
||||
FROM `tabStock Ledger Entry`
|
||||
WHERE
|
||||
item_code = %(item_code)s and warehouse = %(warehouse)s
|
||||
and timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)
|
||||
order by posting_date, posting_time asc """, args, as_dict=1)
|
||||
from pypika import CustomFunction
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty > 0:
|
||||
serial_nos.extend(get_serial_nos_data(d.serial_no))
|
||||
serial_nos = set()
|
||||
args = frappe._dict(args)
|
||||
sle = frappe.qb.DocType('Stock Ledger Entry')
|
||||
Timestamp = CustomFunction('timestamp', ['date', 'time'])
|
||||
|
||||
stock_ledger_entries = frappe.qb.from_(
|
||||
sle
|
||||
).select(
|
||||
'serial_no','actual_qty'
|
||||
).where(
|
||||
(sle.item_code == args.item_code)
|
||||
& (sle.warehouse == args.warehouse)
|
||||
& (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time))
|
||||
& (sle.is_cancelled == 0)
|
||||
).orderby(
|
||||
sle.posting_date, sle.posting_time, sle.creation
|
||||
).run(as_dict=1)
|
||||
|
||||
for stock_ledger_entry in stock_ledger_entries:
|
||||
changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
|
||||
if stock_ledger_entry.actual_qty > 0:
|
||||
serial_nos.update(changed_serial_no)
|
||||
else:
|
||||
serial_nos = list(set(serial_nos) - set(get_serial_nos_data(d.serial_no)))
|
||||
serial_nos.difference_update(changed_serial_no)
|
||||
|
||||
return '\n'.join(serial_nos)
|
||||
|
||||
|
@ -12,12 +12,6 @@ from erpnext.erpnext_integrations.connectors.woocommerce_connection import order
|
||||
|
||||
class TestWoocommerce(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.exists('Company', 'Woocommerce'):
|
||||
company = frappe.new_doc("Company")
|
||||
company.company_name = "Woocommerce"
|
||||
company.abbr = "W"
|
||||
company.default_currency = "INR"
|
||||
company.save()
|
||||
|
||||
woo_settings = frappe.get_doc("Woocommerce Settings")
|
||||
if not woo_settings.secret:
|
||||
@ -26,14 +20,14 @@ class TestWoocommerce(unittest.TestCase):
|
||||
woo_settings.api_consumer_key = "ck_fd43ff5756a6abafd95fadb6677100ce95a758a1"
|
||||
woo_settings.api_consumer_secret = "cs_94360a1ad7bef7fa420a40cf284f7b3e0788454e"
|
||||
woo_settings.enable_sync = 1
|
||||
woo_settings.company = "Woocommerce"
|
||||
woo_settings.tax_account = "Sales Expenses - W"
|
||||
woo_settings.f_n_f_account = "Expenses - W"
|
||||
woo_settings.company = "_Test Company"
|
||||
woo_settings.tax_account = "Sales Expenses - _TC"
|
||||
woo_settings.f_n_f_account = "Expenses - _TC"
|
||||
woo_settings.creation_user = "Administrator"
|
||||
woo_settings.save(ignore_permissions=True)
|
||||
|
||||
def test_sales_order_for_woocommerce(self):
|
||||
frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"Woocommerce","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}
|
||||
frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"_Test Company","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}
|
||||
order()
|
||||
|
||||
self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"}))
|
||||
|
Loading…
Reference in New Issue
Block a user