Merge branch 'develop' into manufacturing-work-order-stop

This commit is contained in:
Anupam Kumar 2021-11-03 13:41:37 +05:30 committed by GitHub
commit 8ebc412e8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1064 additions and 571 deletions

View File

@ -86,4 +86,27 @@ jobs:
cd ~/frappe-bench/ cd ~/frappe-bench/
wget https://erpnext.com/files/v10-erpnext.sql.gz wget https://erpnext.com/files/v10-erpnext.sql.gz
bench --site test_site --force restore ~/frappe-bench/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 bench --site test_site migrate

View File

@ -81,7 +81,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
def identify_is_group(child): def identify_is_group(child):
if child.get("is_group"): if child.get("is_group"):
is_group = 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 is_group = 1
else: else:
is_group = 0 is_group = 0

View File

@ -58,7 +58,8 @@ class GLEntry(Document):
# Update outstanding amt on against voucher # Update outstanding amt on against voucher
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] 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, update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher) self.against_voucher)

View File

@ -58,7 +58,10 @@ class JournalEntry(AccountsController):
if not frappe.flags.in_import: if not frappe.flags.in_import:
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
if not frappe.flags.is_reverse_depr_entry:
self.validate_against_jv() self.validate_against_jv()
self.validate_stock_accounts()
self.validate_reference_doc() self.validate_reference_doc()
if self.docstatus == 0: if self.docstatus == 0:
self.set_against_account() self.set_against_account()
@ -69,7 +72,6 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.set_account_and_party_balance() self.set_account_and_party_balance()
self.validate_inter_company_accounts() self.validate_inter_company_accounts()
self.validate_stock_accounts()
if self.docstatus == 0: if self.docstatus == 0:
self.apply_tax_withholding() self.apply_tax_withholding()

View File

@ -389,7 +389,7 @@ class PaymentEntry(AccountsController):
invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100) 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): if not invoice_paid_amount_map.get(key):
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1])) 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])) (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
else: else:
if allocated_amount > outstanding: 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: if allocated_amount and outstanding:
frappe.db.sql(""" frappe.db.sql("""
@ -1053,12 +1053,6 @@ def get_outstanding_reference_documents(args):
party_account_currency = get_account_currency(args.get("party_account")) party_account_currency = get_account_currency(args.get("party_account"))
company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency") 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 # Get positive outstanding sales /purchase invoices/ Fees
condition = "" condition = ""
if args.get("voucher_type") and args.get("voucher_no"): 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"), 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) 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 data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data: if not data:
@ -1137,22 +1137,26 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
'invoice_amount': flt(d.invoice_amount), 'invoice_amount': flt(d.invoice_amount),
'outstanding_amount': flt(d.outstanding_amount), 'outstanding_amount': flt(d.outstanding_amount),
'payment_amount': payment_term.payment_amount, 'payment_amount': payment_term.payment_amount,
'payment_term': payment_term.payment_term, 'payment_term': payment_term.payment_term
'allocated_amount': payment_term.outstanding
})) }))
outstanding_invoices_after_split = []
if invoice_ref_based_on_payment_terms: if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items(): for idx, ref in invoice_ref_based_on_payment_terms.items():
voucher_no = outstanding_invoices[idx]['voucher_no'] voucher_no = ref[0]['voucher_no']
voucher_type = outstanding_invoices[idx]['voucher_type'] 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) voucher_type, voucher_no, len(ref)), alert=True)
outstanding_invoices.pop(idx - 1) outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
outstanding_invoices += 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, def get_orders_to_be_billed(posting_date, party_type, party,
company, party_account_currency, company_currency, cost_center=None, filters=None): 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 return order_list
def get_negative_outstanding_invoices(party_type, party, party_account, 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" voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = "" supplier_condition = ""
if voucher_type == "Purchase Invoice": if voucher_type == "Purchase Invoice":
@ -1241,19 +1245,21 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
`tab{voucher_type}` `tab{voucher_type}`
where where
{party_type} = %s and {party_account} = %s and docstatus = 1 and {party_type} = %s and {party_account} = %s and docstatus = 1 and
company = %s and outstanding_amount < 0 outstanding_amount < 0
{supplier_condition} {supplier_condition}
{condition}
order by order by
posting_date, name posting_date, name
""".format(**{ """.format(**{
"supplier_condition": supplier_condition, "supplier_condition": supplier_condition,
"condition": condition,
"rounded_total_field": rounded_total_field, "rounded_total_field": rounded_total_field,
"grand_total_field": grand_total_field, "grand_total_field": grand_total_field,
"voucher_type": voucher_type, "voucher_type": voucher_type,
"party_type": scrub(party_type), "party_type": scrub(party_type),
"party_account": "debit_to" if party_type == "Customer" else "credit_to", "party_account": "debit_to" if party_type == "Customer" else "credit_to",
"cost_center": cost_center "cost_center": cost_center
}), (party, party_account, company), as_dict=True) }), (party, party_account), as_dict=True)
@frappe.whitelist() @frappe.whitelist()

View File

@ -4,9 +4,14 @@
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller { erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller {
onload() { 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 { return {
"filters": { "filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)], "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() { this.frm.set_query('receivable_payable_account', () => {
check_mandatory(me.frm);
return { return {
filters: { filters: {
"company": me.frm.doc.company, "company": this.frm.doc.company,
"is_group": 0, "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() { this.frm.set_query('bank_cash_account', () => {
check_mandatory(me.frm, true);
return { return {
filters:[ filters:[
['Account', 'company', '=', me.frm.doc.company], ['Account', 'company', '=', this.frm.doc.company],
['Account', 'is_group', '=', 0], ['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']] ['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() { refresh() {
this.frm.disable_save(); this.frm.disable_save();
this.frm.set_df_property('invoices', 'cannot_delete_rows', true); 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('payments', 'cannot_delete_rows', true);
this.frm.set_df_property('allocation', 'cannot_delete_rows', true); this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
@ -85,76 +76,92 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
} }
company() { company() {
var me = this; this.frm.set_value('party', '');
this.frm.set_value('receivable_payable_account', ''); this.frm.set_value('receivable_payable_account', '');
me.frm.clear_table("allocation"); }
me.frm.clear_table("invoices");
me.frm.clear_table("payments"); party_type() {
me.frm.refresh_fields(); this.frm.set_value('party', '');
me.frm.trigger('party');
} }
party() { party() {
var me = this; this.frm.set_value('receivable_payable_account', '');
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { 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({ return frappe.call({
method: "erpnext.accounts.party.get_party_account", method: "erpnext.accounts.party.get_party_account",
args: { args: {
company: me.frm.doc.company, company: this.frm.doc.company,
party_type: me.frm.doc.party_type, party_type: this.frm.doc.party_type,
party: me.frm.doc.party party: this.frm.doc.party
}, },
callback: function(r) { callback: (r) => {
if (!r.exc && r.message) { 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();
} }
}); });
} }
} }
get_unreconciled_entries() { receivable_payable_account() {
var me = this; this.frm.trigger("clear_child_tables");
return this.frm.call({ this.frm.refresh();
doc: me.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")});
} }
me.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() {
this.frm.clear_table("allocation");
return this.frm.call({
doc: this.frm.doc,
method: 'get_unreconciled_entries',
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")});
}
this.frm.refresh();
} }
}); });
} }
allocate() { allocate() {
var me = this; let payments = this.frm.fields_dict.payments.grid.get_selected_children();
let payments = me.frm.fields_dict.payments.grid.get_selected_children();
if (!(payments.length)) { 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)) { if (!(invoices.length)) {
invoices = me.frm.doc.invoices; invoices = this.frm.doc.invoices;
} }
return me.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: this.frm.doc,
method: 'allocate_entries', method: 'allocate_entries',
args: { args: {
payments: payments, payments: payments,
invoices: invoices invoices: invoices
}, },
callback: function() { callback: () => {
me.frm.refresh(); this.frm.refresh();
} }
}); });
} }
reconcile() { reconcile() {
var me = this; var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
if (show_dialog && show_dialog.length) { if (show_dialog && show_dialog.length) {
@ -186,10 +193,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
label: __("Difference Account"), label: __("Difference Account"),
fieldname: 'difference_account', fieldname: 'difference_account',
reqd: 1, reqd: 1,
get_query: function() { get_query: () => {
return { return {
filters: { filters: {
company: me.frm.doc.company, company: this.frm.doc.company,
is_group: 0 is_group: 0
} }
} }
@ -203,7 +210,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}] }]
}, },
], ],
primary_action: function() { primary_action: () => {
const args = dialog.get_values()["allocation"]; const args = dialog.get_values()["allocation"];
args.forEach(d => { args.forEach(d => {
@ -211,7 +218,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
"difference_account", d.difference_account); "difference_account", d.difference_account);
}); });
me.reconcile_payment_entries(); this.reconcile_payment_entries();
dialog.hide(); dialog.hide();
}, },
primary_action_label: __('Reconcile Entries') primary_action_label: __('Reconcile Entries')
@ -237,15 +244,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
} }
reconcile_payment_entries() { reconcile_payment_entries() {
var me = this;
return this.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: this.frm.doc,
method: 'reconcile', method: 'reconcile',
callback: function(r, rt) { callback: () => {
me.frm.clear_table("allocation"); this.frm.clear_table("allocation");
me.frm.refresh_fields(); this.frm.refresh();
me.frm.refresh();
} }
}); });
} }

View File

@ -114,6 +114,8 @@ class POSInvoiceMergeLog(Document):
def merge_pos_invoice_into(self, invoice, data): def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], [] items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0 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: for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
@ -162,6 +164,11 @@ class POSInvoiceMergeLog(Document):
found = True found = True
if not found: if not found:
payments.append(payment) 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: if loyalty_points_sum:
invoice.redeem_loyalty_points = 1 invoice.redeem_loyalty_points = 1
@ -171,6 +178,10 @@ class POSInvoiceMergeLog(Document):
invoice.set('items', items) invoice.set('items', items)
invoice.set('payments', payments) invoice.set('payments', payments)
invoice.set('taxes', taxes) 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.additional_discount_percentage = 0
invoice.discount_amount = 0.0 invoice.discount_amount = 0.0
invoice.taxes_and_charges = None invoice.taxes_and_charges = None

View File

@ -37,7 +37,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_disposal_account_and_cost_center, get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain, get_gl_entries_on_asset_regain,
post_depreciation_entries, make_depreciation_entry,
) )
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
@ -934,6 +934,7 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
if asset.calculate_depreciation: if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_sale(asset)
self.reset_depreciation_schedule(asset) self.reset_depreciation_schedule(asset)
else: else:
@ -997,22 +998,20 @@ class SalesInvoice(SellingController):
def depreciate_asset(self, asset): def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True 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() asset.save()
post_depreciation_entries(self.posting_date) make_depreciation_entry(asset.name, self.posting_date)
def reset_depreciation_schedule(self, asset): def reset_depreciation_schedule(self, asset):
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset # 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) self.modify_depreciation_schedule_for_asset_repairs(asset)
asset.save() asset.save()
self.delete_depreciation_entry_made_after_sale(asset)
def modify_depreciation_schedule_for_asset_repairs(self, asset): def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all( asset_repairs = frappe.get_all(
'Asset Repair', 'Asset Repair',
@ -1026,7 +1025,7 @@ class SalesInvoice(SellingController):
asset_repair.modify_depreciation_schedule() asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data() 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 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() posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
@ -1041,11 +1040,19 @@ class SalesInvoice(SellingController):
row += 1 row += 1
if schedule.schedule_date == posting_date_of_original_invoice: 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 = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate() reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit() 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): def get_posting_date_of_sales_invoice(self):
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
@ -1060,6 +1067,12 @@ class SalesInvoice(SellingController):
return True return True
return False 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 @property
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):

View File

@ -2237,9 +2237,9 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0) 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() create_asset_data()
@ -2252,7 +2252,7 @@ class TestSalesInvoice(unittest.TestCase):
expected_values = [ expected_values = [
["2020-06-30", 1311.48, 1311.48], ["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.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): 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.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry) 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): def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer, make_customer,

View File

@ -502,8 +502,10 @@ class Subscription(Document):
# Check invoice dates and make sure it doesn't have outstanding invoices # Check invoice dates and make sure it doesn't have outstanding invoices
return getdate() >= getdate(self.current_invoice_start) 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() invoice = self.get_current_invoice()
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) _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): if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
@ -523,7 +525,9 @@ class Subscription(Document):
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice(): if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
self.update_subscription_period(add_days(self.current_invoice_end, 1)) 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') prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate) self.generate_invoice(prorate)
@ -559,14 +563,17 @@ class Subscription(Document):
else: else:
self.set_status_grace_period() 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 # 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() if self.generate_new_invoices_past_due_date and not \
or self.is_prepaid_to_invoice()): 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') prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate) self.generate_invoice(prorate)
if getdate() > getdate(self.current_invoice_end):
self.update_subscription_period(add_days(self.current_invoice_end, 1))
@staticmethod @staticmethod
def is_paid(invoice): def is_paid(invoice):

View File

@ -58,15 +58,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
pan_no = '' pan_no = ''
parties = [] parties = []
party_type, party = get_party_details(inv) party_type, party = get_party_details(inv)
has_pan_field = frappe.get_meta(party_type).has_field("pan")
if not tax_withholding_category: 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: if not tax_withholding_category:
return return
# if tax_withholding_category passed as an argument but not pan_no # 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') pan_no = frappe.db.get_value(party_type, party, 'pan')
# Get others suppliers with the same PAN No # Get others suppliers with the same PAN No

View File

@ -450,7 +450,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# new row with references # new row with references
new_row = journal_entry.append("accounts") 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(d["dr_or_cr"], d["allocated_amount"])
new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit',

View File

@ -75,12 +75,12 @@ class Asset(AccountsController):
if self.is_existing_asset and self.purchase_invoice: if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) 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: if self.calculate_depreciation:
self.value_after_depreciation = 0 self.value_after_depreciation = 0
self.set_depreciation_rate() self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_sale) 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: else:
self.finance_books = [] self.finance_books = []
self.value_after_depreciation = (flt(self.gross_purchase_amount) - self.value_after_depreciation = (flt(self.gross_purchase_amount) -
@ -182,7 +182,7 @@ class Asset(AccountsController):
d.precision("rate_of_depreciation")) d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self, date_of_sale): 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 = [] self.schedules = []
if not self.available_for_use_date: if not self.available_for_use_date:
@ -232,6 +232,7 @@ class Asset(AccountsController):
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date, date_of_sale) from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", { self.append("schedules", {
"schedule_date": date_of_sale, "schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
@ -239,6 +240,7 @@ class Asset(AccountsController):
"finance_book": d.finance_book, "finance_book": d.finance_book,
"finance_book_id": d.idx "finance_book_id": d.idx
}) })
break break
# For first row # For first row
@ -257,11 +259,15 @@ class Asset(AccountsController):
self.to_date = add_months(self.available_for_use_date, self.to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation)) 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, days, months = self.get_pro_rata_amt(d,
depreciation_amount, schedule_date, self.to_date) 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) schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date 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") frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
.format(row.idx)) .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'] straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line']
finance_books = [] finance_books = []
@ -414,7 +441,7 @@ class Asset(AccountsController):
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line # 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] book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation - depreciation_amount += flt(value_after_depreciation -
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) 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 row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life: 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 flt(row.expected_value_after_useful_life)) / depreciation_left
# if the Depreciation Schedule is being modified after Asset Repair # if the Depreciation Schedule is being modified after Asset Repair

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,8 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldname:"status", fieldname:"status",
label: __("Status"), label: __("Status"),
fieldtype: "Select", fieldtype: "Select",
options: "In Location\nDisposed", options: "\nIn Location\nDisposed",
default: 'In Location', default: 'In Location'
reqd: 1
}, },
{ {
"fieldname":"filter_based_on", "fieldname":"filter_based_on",

View File

@ -45,6 +45,7 @@ def get_conditions(filters):
if filters.get('cost_center'): if filters.get('cost_center'):
conditions["cost_center"] = filters.get('cost_center') conditions["cost_center"] = filters.get('cost_center')
if status:
# In Store assets are those that are not sold or scrapped # In Store assets are those that are not sold or scrapped
operand = 'not in' operand = 'not in'
if status not in 'In Location': if status not in 'In Location':

View File

@ -41,10 +41,13 @@ def get_conditions(filters):
if filters.get("from_date") and filters.get("to_date"): if filters.get("from_date") and filters.get("to_date"):
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s" 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): if filters.get(field):
conditions += f" and po.{field} = %({field})s" conditions += f" and po.{field} = %({field})s"
if filters.get('status'):
conditions += " and po.status in %(status)s"
if filters.get('project'): if filters.get('project'):
conditions += " and poi.project = %(project)s" conditions += " and poi.project = %(project)s"

View File

@ -566,7 +566,7 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(['name', query_selector, dimensions]) 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] result = [d.name for d in output]
return [(d,) for d in set(result)] return [(d,) for d in set(result)]

View File

@ -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"]) self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
def calculate_taxes(self): def calculate_taxes(self):
if not self.doc.get('is_consolidated'):
self.doc.rounding_adjustment = 0 self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx # maintain actual tax rate based on idx
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] 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"]) 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 # adjust Discount Amount loss in last tax iteration
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ 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 self.doc.rounding_adjustment = flt(self.doc.grand_total
- flt(self.doc.discount_amount) - tax.total, - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment")) self.doc.precision("rounding_adjustment"))
@ -405,11 +409,16 @@ class calculate_taxes_and_totals(object):
self.doc.rounding_adjustment = diff self.doc.rounding_adjustment = diff
def calculate_totals(self): 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"):
if self.doc.get("taxes") else flt(self.doc.net_total) 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)
if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total 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")) - 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"]) self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
@ -446,6 +455,7 @@ class calculate_taxes_and_totals(object):
self.doc.total_net_weight += d.total_weight self.doc.total_net_weight += d.total_weight
def set_rounded_total(self): def set_rounded_total(self):
if not self.doc.get('is_consolidated'):
if self.doc.meta.get_field("rounded_total"): if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled(): if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = self.doc.base_rounded_total = 0 self.doc.rounded_total = self.doc.base_rounded_total = 0

View File

@ -20,6 +20,7 @@
"website", "website",
"column_break_13", "column_break_13",
"prospect_owner", "prospect_owner",
"company",
"leads_section", "leads_section",
"prospect_lead", "prospect_lead",
"address_and_contact_section", "address_and_contact_section",
@ -153,14 +154,23 @@
"fieldname": "address_and_contact_section", "fieldname": "address_and_contact_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-08-27 16:24:42.961967", "migration_hash": "f39fb8f4e18a0e7fd391f0b4b52d8375",
"modified": "2021-11-01 13:10:36.759249",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Prospect", "name": "Prospect",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -307,6 +307,9 @@ class BOM(WebsiteGenerator):
existing_bom_cost = self.total_cost existing_bom_cost = self.total_cost
for d in self.get("items"): for d in self.get("items"):
if not d.item_code:
continue
rate = self.get_rm_rate({ rate = self.get_rm_rate({
"company": self.company, "company": self.company,
"item_code": d.item_code, "item_code": d.item_code,
@ -599,7 +602,7 @@ class BOM(WebsiteGenerator):
for d in self.get('items'): for d in self.get('items'):
if d.bom_no: if d.bom_no:
self.get_child_exploded_items(d.bom_no, d.stock_qty) self.get_child_exploded_items(d.bom_no, d.stock_qty)
else: elif d.item_code:
self.add_to_cur_exploded_items(frappe._dict({ self.add_to_cur_exploded_items(frappe._dict({
'item_code' : d.item_code, 'item_code' : d.item_code,
'item_name' : d.item_name, 'item_name' : d.item_name,

View File

@ -6,6 +6,8 @@ def execute():
if not company: if not company:
return return
frappe.reload_doc('regional', 'doctype', 'lower_deduction_certificate')
ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc") ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc")
supplier = frappe.qb.DocType("Supplier") supplier = frappe.qb.DocType("Supplier")

View File

@ -31,7 +31,7 @@ class LowerDeductionCertificate(Document):
<= fiscal_year.year_end_date): <= fiscal_year.year_end_date):
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) 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', duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate',
{'supplier': self.supplier, 'tax_withholding_category': self.tax_withholding_category, 'name': ("!=", self.name)}, {'supplier': self.supplier, 'tax_withholding_category': self.tax_withholding_category, 'name': ("!=", self.name)},
['name', 'valid_from', 'valid_upto'], as_dict=True) ['name', 'valid_from', 'valid_upto'], as_dict=True)

View File

@ -855,7 +855,7 @@ def get_depreciation_amount(asset, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life: 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 flt(row.expected_value_after_useful_life)) / depreciation_left
# if the Depreciation Schedule is being modified after Asset Repair # if the Depreciation Schedule is being modified after Asset Repair

View File

@ -122,7 +122,7 @@ def get_total_emiratewise(filters):
try: try:
return frappe.db.sql(""" return frappe.db.sql("""
select 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 from
`tabSales Invoice Item` i inner join `tabSales Invoice` s `tabSales Invoice Item` i inner join `tabSales Invoice` s
on on

View File

@ -206,8 +206,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
var me = this; var me = this;
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if (item.serial_no && item.qty === item.serial_no.split(`\n`).length) { // check if serial nos entered are as much as qty in row
return; 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) { if (item.serial_no && !item.batch_no) {

View File

@ -1195,7 +1195,7 @@
"*": { "*": {
"item_tax_templates": [ "item_tax_templates": [
{ {
"title": "GST 9%", "title": "GST 18%",
"taxes": [ "taxes": [
{ {
"tax_type": { "tax_type": {

View File

@ -676,6 +676,8 @@ class Item(WebsiteGenerator):
def after_rename(self, old_name, new_name, merge): def after_rename(self, old_name, new_name, merge):
if merge: if merge:
self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name) 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: if self.route:
invalidate_cache_for_item(self) invalidate_cache_for_item(self)

View File

@ -88,7 +88,11 @@ frappe.ui.form.on('Stock Entry', {
} }
} }
// 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; filters["warehouse"] = item.s_warehouse || item.t_warehouse;
}
return { return {
query : "erpnext.controllers.queries.get_batch_no", query : "erpnext.controllers.queries.get_batch_no",

View File

@ -600,7 +600,7 @@ class update_entries_after(object):
if not allow_zero_rate: if not allow_zero_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate, 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): def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company # get rate from serial nos within same company
@ -667,7 +667,7 @@ class update_entries_after(object):
if not allow_zero_valuation_rate: if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate, 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): def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate) incoming_rate = flt(sle.incoming_rate)
@ -700,7 +700,7 @@ class update_entries_after(object):
if not allow_zero_valuation_rate: if not allow_zero_valuation_rate:
_rate = get_valuation_rate(sle.item_code, sle.warehouse, _rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate, 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: else:
_rate = 0 _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, 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): 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 last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` force index (item_warehouse) from `tabStock Ledger Entry` force index (item_warehouse)
where where

View File

@ -101,10 +101,6 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
if with_valuation_rate: if with_valuation_rate:
if with_serial_no: 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) return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
@ -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 return last_entry.qty_after_transaction if last_entry else 0.0
def get_serial_nos_data_after_transactions(args): def get_serial_nos_data_after_transactions(args):
serial_nos = [] from pypika import CustomFunction
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)
for d in data: serial_nos = set()
if d.actual_qty > 0: args = frappe._dict(args)
serial_nos.extend(get_serial_nos_data(d.serial_no)) 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: 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) return '\n'.join(serial_nos)

View File

@ -12,12 +12,6 @@ from erpnext.erpnext_integrations.connectors.woocommerce_connection import order
class TestWoocommerce(unittest.TestCase): class TestWoocommerce(unittest.TestCase):
def setUp(self): 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") woo_settings = frappe.get_doc("Woocommerce Settings")
if not woo_settings.secret: if not woo_settings.secret:
@ -26,14 +20,14 @@ class TestWoocommerce(unittest.TestCase):
woo_settings.api_consumer_key = "ck_fd43ff5756a6abafd95fadb6677100ce95a758a1" woo_settings.api_consumer_key = "ck_fd43ff5756a6abafd95fadb6677100ce95a758a1"
woo_settings.api_consumer_secret = "cs_94360a1ad7bef7fa420a40cf284f7b3e0788454e" woo_settings.api_consumer_secret = "cs_94360a1ad7bef7fa420a40cf284f7b3e0788454e"
woo_settings.enable_sync = 1 woo_settings.enable_sync = 1
woo_settings.company = "Woocommerce" woo_settings.company = "_Test Company"
woo_settings.tax_account = "Sales Expenses - W" woo_settings.tax_account = "Sales Expenses - _TC"
woo_settings.f_n_f_account = "Expenses - W" woo_settings.f_n_f_account = "Expenses - _TC"
woo_settings.creation_user = "Administrator" woo_settings.creation_user = "Administrator"
woo_settings.save(ignore_permissions=True) woo_settings.save(ignore_permissions=True)
def test_sales_order_for_woocommerce(self): 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 &times; 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 &times; 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}
order() order()
self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"})) self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"}))